2장. WPF MVVM 실습

목적

  • 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 패턴 구조 모습

  • 실행 결과

728x90

이 글을 공유하기

댓글

Designed by JB FACTORY