2장. WPF MVVM 실습
- C#/WPF
- 2022. 6. 16. 22:49
목적
- WPF MVVM을 학습하고 실습합니다.
요구사항
- 기본 요구사항은 프로젝트 1과 동일합니다.
- 추가 요구사항 : DataGrid Item Double Click 하면 해당 Item 변경 창 출력
MVVM 패턴 이란
- MVVM 패턴이란 ViewModel = View의 출력에 필요한 형태로 데이터를 변환하고 유효화
- View = UI와 DataBinding, Command, 변경 통지 등을 통해 상호작용
- Model - View - ViewModel 패턴은 모든 XAML 플랫폼에서 사용할 수 있다.
- 이 Interface의 목적은 사용자 Interface 컨트롤과 해당 Logic 간의 명확한 분리를 제공하는 것이다.
- MVVM 패턴에는 모델, 보기 및 보기 모델의 세 가지 핵심 구성 요소가 있다.
- 각각은 별개의 역할을 수행한다.
- 구성 요소는 서로 분리뒤어 다음을 가능하게 한다.
- Swap 되는 구성 요소
- 다른 사람에게 영향을 미치지 않고 변경 될 내부 구현
- 독립적으로 작업 할 구성 요소
- 격리 된 단위 테스트
MVVM 이점
- 개발 프로세스 중에 개발자와 디자이너는 구성 요소에 대해 더 독립적으로 동시제 작업할 수 있습니다. 디자이너는 뷰에 집중할 수 있으며 Expression Blend 를 사용하는 경우 작업 할 샘플 데이터를 쉽게 생성할 수 있으며 개발자는 뷰 모델 및 모델 구성 요소에서 작업할 수 있습니다.
- 개발자는 뷰를 사용하지 않고 뷰 모델 및 모델에 대한 단위 테스트를 만들 수 있습니다. 뷰 모델에 대한 단위 테스트는 뷰에서 사용하는 것과 똑같은 기능을 실행할 수 있습니다.
- 보기가 완전히 XAML로 구현되기 때문에 코드를 건드리지 않고 응용 프로그램의 UI를 쉽게 다시 디자인할 수 있습니다. 새 버전의 보기는 기존보기 모델에서 작동해야 합니다.
- 기존 비즈니스 로직을 캡슐화하는 모델의 기존 구현이있는 경우 변경이 어렵거나 위험 할 수 있습니다. 이 시나리오에서 보기 모델은 모델 클래스의 어댑터 역할을 하며 모델 코드에 대한 주요 변경을 방지할 수 있습니다.
실습
- 요구사항 대로 프로젝트를 생성하여 실습합니다.
Model
- Person.cs
using ReactiveUI;
namespace MVVM
{
public class Person : ReactiveObject
{
private string _name;
public string Name
{
get { return _name; }
set { this.RaiseAndSetIfChanged(ref _name, value); }
}
private bool _gender;
public bool Gender
{
get { return _gender; }
set { this.RaiseAndSetIfChanged(ref _gender, value); }
}
private string _phoneNumber;
public string PhoneNumber
{
get { return _phoneNumber; }
set { this.RaiseAndSetIfChanged(ref _phoneNumber, value); }
}
private string _address;
public string Address
{
get { return _address; }
set { this.RaiseAndSetIfChanged(ref _address, value); }
}
public Person()
{
}
public Person(Person input)
{
_address = input.Address;
_name = input.Name;
_phoneNumber = input.PhoneNumber;
_gender = input.Gender;
}
}
}
## **View**
* **AddView.xaml**
```c#
<Window x:Class="MVVM.AddView"
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:MVVM"
mc:Ignorable="d" Name="AddViewParam"
d:DataContext="{d:DesignInstance local:AddView}"
Title="{Binding Path=Caption,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}"
Height="300" Width="300"
WindowStartupLocation="CenterScreen"
WindowStyle="ToolWindow">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="4"/>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="*"/>
<RowDefinition Height="4"/>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="53"/>
<ColumnDefinition Width="75"/>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="75"/>
<ColumnDefinition Width="4"/>
</Grid.ColumnDefinitions>
<Label Content="이름" VerticalAlignment="Stretch"
Grid.Column="1" Grid.Row="1"/>
<TextBox TextWrapping="Wrap" Grid.Column="3"
Grid.Row="1" Grid.ColumnSpan="4"
Text="{Binding Path=PersonData.Name,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}"/>
<Label Content="성별" Grid.Column="1" Grid.Row="3"/>
<TextBox TextWrapping="Wrap" Grid.Column="3" Grid.Row="3"
Grid.ColumnSpan="4"
Text="{Binding Path=PersonData.Gender,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}"/>
<Label Content="전화번호" Grid.Column="1" Grid.Row="5"/>
<TextBox TextWrapping="Wrap" Grid.Column="3" Grid.Row="5"
Grid.ColumnSpan="4"
Text="{Binding Path=PersonData.PhoneNumber,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}"/>
<Label Content="주소" VerticalAlignment="Center"
Grid.Column="1" Grid.Row="7"/>
<TextBox TextWrapping="Wrap" Grid.Column="3" Grid.Row="7"
Grid.ColumnSpan="4"
Text="{Binding Path=PersonData.Address,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}"/>
<Button IsDefault="True" Content="확인" Grid.Column="4"
Grid.Row="9" Command="{Binding OkCommand}"
CommandParameter="{Binding ElementName=AddViewParam}"/>
<Button Content="취소" Grid.Column="6" Grid.Row="9"
Command="{Binding CancelCommand}"
CommandParameter="{Binding ElementName=AddViewParam}"/>
</Grid>
</Window>
- AddView.xaml.cs
using System.Windows;
namespace MVVM
{
/// <summary>
/// AddView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class AddView : Window, IDialogView
{
public AddView(AddViewModel viewModel)
{
InitializeComponent();
this.DataContext = viewModel;
}
}
}
- MainView.xaml
<Window x:Class="MVVM.MainView"
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:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:local="clr-namespace:MVVM"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:MainViewModel}"
Title="주소록 관리 V0.1" Height="328" Width="525"
WindowStartupLocation="CenterScreen" Name="MainViewParam">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="182*"/>
<ColumnDefinition Width="75"/>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="75"/>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="75"/>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="75"/>
<ColumnDefinition Width="4"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="4"/>
<RowDefinition Height="244*"/>
<RowDefinition Height="4"/>
<RowDefinition Height="20"/>
<RowDefinition Height="4"/>
</Grid.RowDefinitions>
<!--SelectedIndex="{Binding Path=SelectIndex, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay }"-->
<DataGrid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{Binding Path=Persons, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
SelectionMode="Single" AutoGenerateColumns="False" AlternationCount="2"
AlternatingRowBackground="Gainsboro" IsReadOnly="True"
Name="PersonsParam" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="8">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding ModifyCommand}"
CommandParameter="{Binding ElementName=PersonsParam, Path=SelectedIndex}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<DataGrid.Columns>
<DataGridTextColumn Header="이름" Binding="{Binding Path=Name}" Width="100"/>
<DataGridTextColumn Header="성별" Binding="{Binding Path=Gender}"/>
<DataGridTextColumn Header="전화번호" Binding="{Binding Path=PhoneNumber}" Width="100"/>
<DataGridTextColumn Header="주소" Binding="{Binding Path=Address}" Width="200"/>
</DataGrid.Columns>
</DataGrid>
<Button Command ="{Binding Path=AddCommand}"
Content="추가" Width="75" Grid.Row="3" Grid.Column="2" />
<Button Command ="{Binding Path=ModifyCommand}"
CommandParameter="{Binding ElementName=PersonsParam, Path=SelectedIndex}"
Content="변경" Width="75" Grid.Row="3" Grid.Column="6" />
<Button Command ="{Binding Path=DeleteCommand}"
CommandParameter="{Binding ElementName=PersonsParam, Path=SelectedIndex}"
Content="삭제" Width="75" Grid.Row="3" Grid.Column="4" />
<Button Command="{Binding ExitCommand}"
CommandParameter="{Binding ElementName=MainViewParam}"
Content="종료" Width="75" Grid.Row="3" Grid.Column="8" />
</Grid>
</Window>
- MainView.xaml.cs
using System.Windows;
namespace MVVM
{
/// <summary>
/// MainView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class MainView : Window, IWindowView
{
public MainView(MainViewModel viewModel)
{
InitializeComponent();
this.DataContext = viewModel;
}
}
}
ViewModel
- AddViewModel.cs
using ReactiveUI;
using System.Reactive;
using System.Windows.Input;
namespace MVVM
{
// ReactiveObjext 상속 받는다 : Property 값이 변경될 때 UI에 알려준다.
public class AddViewModel : ReactiveObject
{
public enum ViewType
{
Add,
Modify
}
private string _caption;
public string Caption
{
get { return _caption; }
set { this.RaiseAndSetIfChanged(ref _caption, value); }
}
public Person PersonData { get; set; }
public ICommand OkCommand { get; private set; }
public ICommand CancelCommand { get; private set; }
public AddViewModel(Person Data = null, ViewType type = ViewType.Add)
{
if (type == ViewType.Add)
{
Caption = "추가";
}
else if (type == ViewType.Modify)
{
Caption = "변경";
}
else
{
Caption = "추가";
}
PersonData = Data;
OkCommand = ReactiveCommand.Create<IDialogView, Unit>(view => _okCommandAction(view));
CancelCommand = ReactiveCommand.Create<IDialogView, Unit>(view => _cancelCommandAction(view));
}
private Unit _okCommandAction(IDialogView view)
{
view.DialogResult = true;
view.Close();
return Unit.Default;
}
private Unit _cancelCommandAction(IDialogView view)
{
view.DialogResult = false;
view.Close();
return Unit.Default;
}
}
}
- MainViewModel.cs
using ReactiveUI;
using System;
using System.Collections.ObjectModel;
using System.Reactive;
using System.Windows;
using System.Windows.Input;
namespace MVVM
{
public class MainViewModel : ReactiveObject
{
public ObservableCollection<Person> Persons { get; set; }
public ICommand AddCommand { get; set; }
public ICommand DeleteCommand { get; set; }
public ICommand ModifyCommand { get; set; }
public ICommand ExitCommand { get; set; }
private readonly IMessageBoxService _messageBoxService;
private readonly Func<Person, AddViewModel.ViewType, IDialogView> _createAddView;
public MainViewModel(IMessageBoxService messageBoxService
, Func<Person, AddViewModel.ViewType, IDialogView> createAddView)
{
_messageBoxService = messageBoxService;
_createAddView = createAddView;
Persons = new ObservableCollection<Person>();
_initCommand(); ;
_initTestData();
}
private void _initTestData()
{
Persons.Add(new Person() { Address = "test1", Name = "홍길동1", Gender = true, PhoneNumber = "11111" });
Persons.Add(new Person() { Address = "test2", Name = "홍길동2", Gender = true, PhoneNumber = "22222" });
Persons.Add(new Person() { Address = "test3", Name = "홍길동3", Gender = true, PhoneNumber = "33333" });
Persons.Add(new Person() { Address = "test4", Name = "홍길동4", Gender = true, PhoneNumber = "44444" });
}
private void _initCommand()
{
AddCommand = ReactiveCommand.Create(_addCommandAction);
DeleteCommand = ReactiveCommand.Create<int, Unit>(index => _deleteCommandAction(index));
ModifyCommand = ReactiveCommand.Create<int, Unit>(index => _modifyCommandAction(index));
ExitCommand = ReactiveCommand.Create<IWindowView, Unit>(view => _exitCommandAction(view));
}
private Unit _exitCommandAction(IWindowView view)
{
view.Close();
return Unit.Default;
}
private Unit _modifyCommandAction(int selectedIndex)
{
if (selectedIndex < 0)
{
_messageBoxService.Show("선택된 데이터가 없습니다.", "주소록 v0.1", MessageBoxButton.OK, MessageBoxImage.Information);
return Unit.Default;
}
var modifyData = new Person(Persons[selectedIndex]);
IDialogView view = _createAddView(modifyData, AddViewModel.ViewType.Modify);
if (true == view.ShowDialog())
{
Persons[selectedIndex] = modifyData;
}
return Unit.Default;
}
private Unit _deleteCommandAction(int selectedIndex)
{
if (selectedIndex < 0)
{
_messageBoxService.Show("선택된 데이터가 없습니다.", "주소록 v0.1", MessageBoxButton.OK, MessageBoxImage.Information);
return Unit.Default;
}
var result = _messageBoxService.Show("선택된 데이터가 삭제 하시겠습니까?", "주소록 v0.1", MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (result == MessageBoxResult.Cancel)
return Unit.Default;
Persons.RemoveAt(selectedIndex);
return Unit.Default;
}
private void _addCommandAction()
{
var addData = new Person();
IDialogView view = _createAddView(addData, AddViewModel.ViewType.Add);
if (true == view.ShowDialog())
{
Persons.Add(addData);
}
}
}
}
Interface
- IDialogView.cs
namespace MVVM
{
public interface IDialogView
{
bool? ShowDialog();
bool? DialogResult { get; set; }
void Show();
void Close();
}
}
- IMessageBoxService.cs
using System.Windows;
namespace MVVM
{
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(System.Windows.Window owner, string messageBoxText);
MessageBoxResult Show(System.Windows.Window owner, string messageBoxText, string caption);
MessageBoxResult Show(System.Windows.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);
}
}
* **IWindow.View.cs**
```c#
using System.Windows;
namespace MVVM
{
public interface IWindowView
{
void Show();
void Close();
Visibility Visibility { get; set; }
}
}
Service
- App.xaml
<Application x:Class="MVVM.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MVVM">
<Application.Resources>
</Application.Resources>
</Application>
- App.xaml.cs
using System.Windows;
namespace MVVM
{
///
/// App.xaml에 대한 상호 작용 논리
///
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
IMessageBoxService messageBoxService = new MessageBoxService();
MainViewModel mainViewModel = new MainViewModel(messageBoxService, _createAddView);
MainView mainView = new MainView(mainViewModel);
mainView.Show();
}
protected override void OnExit(ExitEventArgs e)
{
base.OnExit(e);
}
private IDialogView _createAddView(Person modifyData, AddViewModel.ViewType type = AddViewModel.ViewType.Add)
{
//IMessageBoxService messageBoxService = new MessageBoxService();
var viewModel = new AddViewModel(modifyData, type);
return new AddView(viewModel);
}
}
* **MessageBoxService.cs**
```c#
using System.Windows;
namespace MVVM
{
public class MessageBoxService : 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 Show(parentWindow, messageBoxText);
}
public MessageBoxResult Show(DependencyObject owner, string messageBoxText, string caption)
{
Window parentWindow = Window.GetWindow(owner);
return Show(parentWindow, messageBoxText, caption);
}
public MessageBoxResult Show(DependencyObject owner, string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon)
{
Window parentWindow = Window.GetWindow(owner);
return 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);
}
}
}
- MVVM 패턴 구조 모습
- 실행 결과
'C# > WPF' 카테고리의 다른 글
[WPF] WPF DevExpress 실습 (2) | 2022.06.17 |
---|---|
[WPF] WPF DevExpress.ReactiveUI 실습 (0) | 2022.06.17 |
1장. WPF Binding 이용하여 프로젝트 실습 (0) | 2022.06.16 |
[WPF] WPF 점선 그리기 (0) | 2022.01.06 |
[WPF 기본 문법] WPF TextBlock, Button 컨트롤 Style 사용하기 (0) | 2021.12.28 |
이 글을 공유하기