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() 함수 호출 이후에 오류가 발생하였다.