본문 바로가기
Study/WPF

[WPF][MVVM][Study] MVVM 실습 4 - 주소록 만들기

by 스테디코디스트 2023. 9. 27.
반응형
MVVM을 이용해 주소록을 만드는 과정을 실습하였다.

 

⚠️주의 : dll 파일 오류로 인해 실행이 안될 수 있음!

 

0. 파일 구조

- 파일 구조는 아래와 같다.

 

1. MainView[View]

1) xaml

<!--MainView.xaml-->
<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF_MVVM_EX_4_2.View"
        xmlns:ViewModel="clr-namespace:WPF_MVVM_EX_4_2.ViewModel" x:Class="WPF_MVVM_EX_4_2.View.MainView"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="500">

    <Window.DataContext>
        <ViewModel:MainViewModel/>
    </Window.DataContext>

    <Grid>
        <DataGrid HorizontalAlignment="Left" Height="250" VerticalAlignment="Top" Width="500" AutoGenerateColumns="False"
                  ItemsSource="{Binding Persons, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                  SelectionMode="Single" IsReadOnly="True">

            <DataGrid.Columns>
                <DataGridTextColumn Header="이름" Width="50" Binding="{Binding Model.Name}"/>
                <DataGridTextColumn Header="성별" Width="50" Binding="{Binding Model.Gender}"/>
                <DataGridTextColumn Header="휴대폰" Width="150" Binding="{Binding Model.PhoneNumber}"/>
                <DataGridTextColumn Header="주소" Width="240" Binding="{Binding Model.Address}"/>
            </DataGrid.Columns>

        </DataGrid>

        <WrapPanel HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,8,8" Height="50" Width="360">
            <Button Height="30" Width="70" Margin="10" Content="추가" Command="{Binding AddCommand}"/>
            <Button Height="30" Width="70" Margin="10" Content="삭제" Command="{Binding DelCommand}"/>
            <Button Height="30" Width="70" Margin="10" Content="변경" Command="{Binding ChangeCommand}"/>
            <Button Height="30" Width="70" Margin="10" Content="나가기" Command="{Binding ExitCommand}"/>
        </WrapPanel>

    </Grid>

</Window>

2) code-behind

// MainView.xaml.cs
using System.Windows;
using WPF_MVVM_EX_4_2.Interface;

namespace WPF_MVVM_EX_4_2.View
{
    /// <summary>
    /// MainView.xaml에 대한 상호 작용 논리
    /// </summary>
    public partial class MainView : Window , IWindowView // 인터페이스 IWindowView 상속
    {
    	// 생성자
        public MainView(ViewModel.MainViewModel viewModel)
        {
            InitializeComponent();
            this.DataContext = viewModel; // DataContext를 뷰모델과 연결
        }
    }
}

3) 설명

- DataGrid를 이용해 MainView를 생성해주었다.


2. SubView [View]

1) xaml

<!--SubView.xaml-->
<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF_MVVM_EX_4_2.View"
        xmlns:ViewModel="clr-namespace:WPF_MVVM_EX_4_2.ViewModel" x:Class="WPF_MVVM_EX_4_2.View.SubView"
        mc:Ignorable="d"
        Title="{Binding Caption, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
        Height="300" Width="300">

    <Window.DataContext>
        <ViewModel:SubViewModel/>
    </Window.DataContext>

    <Grid>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="4*"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="5*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <TextBlock Text="이름" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <TextBlock Text="성별" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <TextBlock Text="휴대폰" Grid.Row="2" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <TextBlock Text="주소" Grid.Row="3" HorizontalAlignment="Center" VerticalAlignment="Center"/>

        <TextBox Margin="5" Grid.Column="1" 
                 Text="{Binding PersonData.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

        <WrapPanel Grid.Column="1" Grid.Row="1" VerticalAlignment="Center">
            <RadioButton Margin="4,0,2,0"/>
            <TextBlock Text="남성" Margin="2,0,2,0"/>
            <RadioButton Margin="2,0,2,0"/>
            <TextBlock Text="여성" Margin="2,0,2,0"/>
        </WrapPanel>

        <TextBox Margin="5" Grid.Column="1" Grid.Row="2" Text="{Binding PersonData.PhoneNumber}"/>
        <TextBox Margin="5" Grid.Column="1" Grid.Row="3" Text="{Binding PersonData.Address}"/>

        <WrapPanel Grid.Row="4" Grid.ColumnSpan="2" HorizontalAlignment="Center">
            <Button Margin="5,5,5,5" Width="100" Content="확인" Command="{Binding OkCommand}"/>
            <Button Margin="5,5,5,5" Width="100" Content="취소" Command="{Binding ExitCommand}"/>
        </WrapPanel>

    </Grid>

</Window>

2) code-behind

// SubView.xaml.cs
using System.Windows;
using WPF_MVVM_EX_4_2.Interface;

namespace WPF_MVVM_EX_4_2.View
{
    /// <summary>
    /// SubView.xaml에 대한 상호 작용 논리
    /// </summary>
    public partial class SubView : Window, IDialogView // 인터페이스 IDialogView 상속
    {
    	// 생성자    	
        public SubView(ViewModel.SubViewModel viewModel)
        {
            InitializeComponent();
            this.DataContext = viewModel; // DataContext를 뷰모델과 연결
        }
    }
}

3) 설명

- 주소록 추가 또는 주소록 변경을 위한 SubView를 생성해주었다.

 

3. Person[Model]

1) code

// Person.cs : Model
using System;
using ReactiveUI;

namespace WPF_MVVM_EX_4_2.Model
{
	// ReactiveUI : 변경된 값이 UI에 적용되도록 Notify~를 상속받아서 사용자들이 사용하기 쉽게 만들어 놓은 라이브러리
    public class Person : ReactiveObject // ReactiveUI를 using하여 상속받아 사용한다.
    {
        private string _name;
        public string Name
        {
            get => _name;
            set
            {
            	// 값이 바뀌면 UI에 적용되도록 한다.
                this.RaiseAndSetIfChanged(ref _name, value);
            }
        }

        private bool _gender;
        public bool Gender
        {
            get => _gender;
            set
            {
                this.RaiseAndSetIfChanged(ref _gender, value);
            }
        }

        private string _phoneNumber;
        public string PhoneNumber
        {
            get => _phoneNumber;
            set
            {
                this.RaiseAndSetIfChanged(ref _phoneNumber, value);
            }
        }

        private string _address;
        public string Address
        {
            get => _address;
            set
            {
                this.RaiseAndSetIfChanged(ref _address, value);
            }
        }

        public Person() { }

        public Person(Person input)
        {
            // 생성자 -> 현재 값들을 input의 데이터로 변경
            _address = input.Address;
            _gender = input.Gender;
            _phoneNumber = input.PhoneNumber;
            _address = input.Address;
        }
    }
}

2) 설명

- 이름,성별,휴대폰,주소 이렇게 4가지 속성을 사용하는 Person 클래스를 정의하였다.

- MVVM패턴에서 Model의 역할을 수행한다.

- ReactiveUI를 사용하였다.

 

4. MainViewModel[ViewModel]

1) code

// MainViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using WPF_MVVM_EX_4_2.Interface;
using WPF_MVVM_EX_4_2.Model;
using ReactiveUI;
using System.Reactive;
using static WPF_MVVM_EX_4_2.ViewModel.SubViewModel;

namespace WPF_MVVM_EX_4_2.ViewModel
{
    public class MainViewModel : ReactiveObject
    {
        public ObservableCollection<Person> Persons { get; set; } // 모든 정보들을 담고있음

        // 각 버튼에 연결되는 커맨드
        public ICommand AddCommand { get; set; } // 추가 커맨드
        public ICommand DelCommand { get; set; } // 삭제
        public ICommand ChangeCommand { get; set; } // 변경
        public ICommand ExitCommand { get; set; } // 나가기

        private readonly IMessageBoxService _messageBoxService; // 메세지 창 서비스
        private readonly Func<Person, ViewType, IDialogView> _createSubViewModel; // SubViewModel을 생성하는 delegate

        public MainViewModel() { } // 기본 생성자 -> 선언하지 않으면 바인딩이 되지 않음!?

        public MainViewModel(IMessageBoxService messageBoxService, Func<Person, ViewType, IDialogView> createSubViewModel)
        {
            // 생성자
            _messageBoxService = messageBoxService; // 메세지박스 연결
            _createSubViewModel = createSubViewModel; // 서브 뷰모델 생성 함수 연결
            
            Persons = new ObservableCollection<Person>(); // 첫 데이터 생성
            _initCommand(); // 명령 생성
            _initTestData();
        }

        private void _initCommand()
        {
            // 각 명령들을 생성
            AddCommand = ReactiveCommand.Create(_addCommandAction);
            DelCommand = ReactiveCommand.Create<int, Unit>(index => _delCommandAction(index));
            ChangeCommand = ReactiveCommand.Create<int, Unit>(index => _changeCommandAction(index));
            ExitCommand = ReactiveCommand.Create<IWindowView, Unit>(view => _exitCommandAction(view));
        }

        private void _initTestData()
        {
            Persons.Add(new Person() { Address = "test", Name = "홍길동", Gender = true, PhoneNumber = "012345" });
        }

        private void _addCommandAction()
        {
            // 1. 추가
            Person addData = new Person(); // 추가할 데이터 생성

            IDialogView view = _createSubViewModel(addData, ViewType.Add); // 생성된 SubView 지정(할당)

            // Q. 창이 뜬 동안 데이터를 넣어주는 거면 while이 되어야 하는게 아닌가? -> A. 함수 내부적으로 창이 닫혀야  false를 반환하도록 구현된 듯
            if (true == view.ShowDialog())
            {
                Persons.Add(addData); // 추가 데이터를 현재 데이터 리스트에 추가
            }
        }

        private Unit _delCommandAction(int selectedIndex)
        {
            // 2. 삭제
            if (selectedIndex < 0)
            {
                _messageBoxService.Show("선택된 데이터가 없습니다.", "주소록 v.1.0", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
                return Unit.Default;
            }

            var result = _messageBoxService.Show("선택된 데이터를 삭제 하시겠습니까?", "주소록 v.1.0", System.Windows.MessageBoxButton.OKCancel, System.Windows.MessageBoxImage.Question);

            // 삭제를 취소한 경우
            if (result == System.Windows.MessageBoxResult.Cancel) return Unit.Default;

            // 삭제하는 경우
            Persons.RemoveAt(selectedIndex); // 선택한 데이터를 삭제
            return Unit.Default;
        }

        private Unit _changeCommandAction(int selectedIndex)
        {
            // 3. 변경
            if (selectedIndex < 0)
            {
                _messageBoxService.Show("선택된 데이터가 없습니다.", "주소록 v.1.0", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
                return Unit.Default;
            }

            Person changeData = new Person(Persons[selectedIndex]); // 현재 선택한 데이터와 같은 데이터를 새로 생성해 변해야되는 데이터로 지정(할당)

            IDialogView view = _createSubViewModel(changeData, ViewType.Change); // 생성된 SubView를 지정(할당)

            // Q. 창이 뜬 동안 데이터를 넣어주는 거면 while이 되어야 하는게 아닌가?
            if (true == view.ShowDialog())
            {
                Persons[selectedIndex] = changeData; // 변경된 데이터를 현재 데이터에 넣어줌
            }

            return Unit.Default;
        }

        private Unit _exitCommandAction(IWindowView view)
        {
            // 4. 나가기
            view.Close(); // 윈도우 창 닫음
            return Unit.Default;
        }        
    }
}

2) 설명

- 각 버튼에 대한 커맨드를 정의했다.

 

5. SubViewModel[ViewModel]

1) code

// SubViewModel.cs
using System.Windows.Input;
using ReactiveUI;
using System.Reactive;
using WPF_MVVM_EX_4_2.Interface;

namespace WPF_MVVM_EX_4_2.ViewModel
{
    public class SubViewModel : ReactiveObject
    {
        public enum ViewType // 윈도우 창 타입
        {
            Add, // 추가 창
            Change // 변경 창
        }

        private string _caption; // 윈도우 창 이름
        public string Caption
        {
            get => _caption;
            set
            {
                // 값이 바뀌면 UI에 바뀐 내용으로 표시해줌
                this.RaiseAndSetIfChanged(ref _caption, value);
            }
        }

        public Model.Person PersonData { get; set; } // 현재 모델의 데이터
        
        public ICommand OkCommand { get; private set; } // 확인 커맨드

        public ICommand CancelCommand { get; private set; } // 취소 커맨드

        public SubViewModel() { } // 기본 생성자 -> 선언하지 않으면 바인딩이 되지 않음!?

        public SubViewModel(Model.Person data = null, ViewType type = ViewType.Add) // 모델의 데이터와 생성된 윈도우 타입을 매개변수로 가지는 생성자
        {
            // 윈도우 창 이름 변경
            if (type == ViewType.Add)
            {
                Caption = "추가";
            }
            else if (type == ViewType.Change)
            {
                Caption = "변경";
            }
            else
            {
                Caption = "오류";
            }

            PersonData = data; // 원래의 데이터가 있다면 데이터를 받아서 현재 창에 띄움('변경'의 경우)

            // 커맨드를 넣어줌
            // ReactiveCommand.Creat<TInput, Toutput> 
            // : Func<TInput, Toutput> 처럼 동작하거나 Action<T> 처럼 동작
            
             // IDialogView를 입력으로 하고, Unit을 출력으로 하는 delegate(Func과 같이 동작)
             // view를 입력으로 받아 _okCommandAction(view)를 출력함.     
            OkCommand = ReactiveCommand.Create<IDialogView, Unit>(view => _okCommandAction(view));
            CancelCommand = ReactiveCommand.Create<IDialogView, Unit>(view => _cancelCommandAction(view)); // view를 입력으로 받아 _cancelCommandAction(view)를 출력함. 
        }

        private Unit _okCommandAction(IDialogView view)
        {
            view.DialogResult = true; // 결과를 true로 하여 '확인'버튼을 눌렀음을 알림
            view.Close(); // 창을 닫음

            return Unit.Default; // 현재 Unit이 Defualt와 같은지를 확인(변경되었는지 확인?)
        }

        private Unit _cancelCommandAction(IDialogView view)
        {
            view.DialogResult = false; // 결과를 false로 하여 '취소'버튼을 눌렀음을 알림
            view.Close(); // 창을 닫음

            return Unit.Default;
        }
    }
}

2) 설명

- SubView 창에 이름 설정 방법 과, 명령들을 정의했다.

 

6. Application

- MainView 창을 띄울 때까지의 로직을 구현

// App.xaml.cs
using System.Windows;
using WPF_MVVM_EX_4_2.Model;
using WPF_MVVM_EX_4_2.ViewModel;
using WPF_MVVM_EX_4_2.Interface;
using WPF_MVVM_EX_4_2.View;

namespace WPF_MVVM_EX_4_2
{
    /// <summary>
    /// App.xaml에 대한 상호 작용 논리
    /// </summary>
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            IMessageBoxService messageBoxService = new Service.MessageBoxService();
            
            // MainViewModel(메인 뷰모델) 생성
            MainViewModel mainViewModel = new MainViewModel(messageBoxService, _createSubViewModel); 

            // 메인 윈도우 생성 후 창을 띄움
            MainView mainView = new MainView(mainViewModel);
            mainView.Show();
        }

        protected override void OnExit(ExitEventArgs e)
        {
            base.OnExit(e);
        }

        private IDialogView _createSubViewModel(Person changeData = null, SubViewModel.ViewType type = SubViewModel.ViewType.Add)
        {
        	// SubViewModel(서브 뷰모델) 생성
            SubViewModel subViewModel = new SubViewModel(changeData, type); 

            // 서브 뷰모델과 연결된 새로운 뷰 윈도우를 생성 후 반환
            return new SubView(subViewModel); 
        }
    }
}

 

7. Interface

1) IDialogView

- SubView에 상속되어 MainView와 SubView가 독립적으로 동작하도록 한다.

// IDialogView.cs
namespace WPF_MVVM_EX_4_2.Interface
{
	// SubView에 상속되어 동작한다.
    public interface IDialogView
    {
        bool? ShowDialog();

        bool? DialogResult { get; set; }

        void Show();

        void Close();
    }
}

2) IWindowView

- MainView에 상속되어 동작하는 인터페이스 이다.

// IWindowView.cs
using System.Windows;

namespace WPF_MVVM_EX_4_2.Interface
{
	// MainView에 상속되어 동작
    public interface IWindowView
    {
        void Show();
        void Close();

        Visibility Visibility { get; set; }
    }
}

 

3) IMessageBoxService

- 버튼을 잘못 누르는 등의 메세지를 띄우기 위해 사용

- MainView와 SubView에 따른 함수 선언

// IMessageBoxService.cs
using System.Windows;

namespace WPF_MVVM_EX_4_2.Interface
{
	// MessageBox 동작을 수행
    public interface IMessageBoxService
    {
        MessageBoxResult Show(string messageBoxText);
        MessageBoxResult Show(string messageBoxText, string caption);
        MessageBoxResult Show(string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon);

        MessageBoxResult Show(Window owner, string messageBoxText);
        MessageBoxResult Show(Window owner, string messageBoxText, string caption);
        MessageBoxResult Show(Window owner, string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon);

        MessageBoxResult Show(DependencyObject owner, string messageBoxText);
        MessageBoxResult Show(DependencyObject owner, string messageBoxText, string caption);
        MessageBoxResult Show(DependencyObject owner, string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon);

        MessageBoxResult Show(IWindowView owner, string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon);
    }
}

 

 8. MessageBoxService[Service]

- IMessageBoxService를 상속받아 해당 메세지 동작을 구현했다.

// MessageBoxService.cs
using System.Windows;
using WPF_MVVM_EX_4_2.Interface;

namespace WPF_MVVM_EX_4_2.Service
{
    class MessageBoxService : Interface.IMessageBoxService
    {
        public MessageBoxResult Show(string messageBoxText)
        {
            return MessageBox.Show(messageBoxText);
        }

        public MessageBoxResult Show(string messageBoxText, string caption)
        {
            return MessageBox.Show(messageBoxText, caption);
        }

        public MessageBoxResult Show(string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon)
        {
            return MessageBox.Show(messageBoxText, caption, button, icon);
        }

        public MessageBoxResult Show(Window owner, string messageBoxText)
        {
            return MessageBox.Show(owner, messageBoxText);
        }

        public MessageBoxResult Show(Window owner, string messageBoxText, string caption)
        {
            return MessageBox.Show(owner, messageBoxText, caption);
        }

        public MessageBoxResult Show(Window owner, string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon)
        {
            return MessageBox.Show(owner, messageBoxText, caption, button, icon);
        }

        public MessageBoxResult Show(DependencyObject owner, string messageBoxText)
        {
            Window parentWindow = Window.GetWindow(owner);
            return MessageBox.Show(parentWindow, messageBoxText);
        }

        public MessageBoxResult Show(DependencyObject owner, string messageBoxText, string caption)
        {
            Window parentWindow = Window.GetWindow(owner);
            return MessageBox.Show(parentWindow, messageBoxText, caption);
        }

        public MessageBoxResult Show(DependencyObject owner, string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon)
        {
            Window parentWindow = Window.GetWindow(owner);
            return MessageBox.Show(parentWindow, messageBoxText, caption, button, icon);
        }

        public MessageBoxResult Show(IWindowView owner, string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon)
        {
            Window window = owner as Window;
            return MessageBox.Show(window, messageBoxText, caption, button, icon);
        }
    }
}

 


9. 오류

- MainView를 생성시키는 application단에서 mainView.Show() 함수 호출 이후에 오류가 발생하였다.