이번 포스트는 간단하게 MVVM Pattern에 대해서 정리를 하고, 사용했을 때의 장점을 살펴 보도록 하겠다.

 

참고 포스트

MVVM Pattern Made Simple

http://www.codeproject.com/Articles/278901/MVVM-Pattern-Made-Simple

 

1. MVVM Pattern이란?

MVVM은 Model-View-ViewModel의 약자이며, 각 단어의 의미에 대해서 살펴보자.

1) Model : 서비스나 database에서 받아오는 data의 집합 모델이나, data를 CRUD하는 메소드를 포함할 수 있다.

2) View : 사용자가 보는 UI로 구성된 화면을 이야기 하며, xaml로 구성되어 있다.(WPF, Silverlight, WP7, Metro style)

3) ViewModel : View의 Model이란 의미로, View와 Model 사이에 위치하며, 사용자의 interaction에 의해 data를 CRUD하는 business logic을 가지고 있는 클래스이다.

 

2. MVVM Pattern을 사용할 때의 장점은?

0) 진정한 OOP를 지향한다.

모델은 Data를 object로 만드는 틀이되고, 그 틀로 만들어 놓은 object를 뷰모델을 통해서 처리를 해서 뷰에 출력하는 방식으로, 예를들어 홍길동이라는 object가 존재하고, 홍길동의 이름이 보이는 프로필 화면과, 리스트 화면 2가지가 있다고 가정해보자.

프로필 화면에서 홍길동의 이름을 홍길순으로 변경을 하면, 변경된 내용이 바인딩된 object에 이름이라는 프로퍼티를 수정하게되고, 이름 프로퍼티가 수정이 되면 그 이름 프로퍼티에 바인딩이 되어있던 다른 화면의 홍길동이란 이름도 홍길순으로 즉시 변경된다.

이렇게 바인딩으로 연결된 구조에서는 object와 view가 stream으로 연결되어, data가 흘러 다닌다.

 

1) View와 business logic의 느슨한 연결(loosly coupled) :

두개가 서로 느슨한 연결을 가지고 있으므로, 뷰나 비지니스로직 둘 중 하나가 변경이 되더라도 다른 부분에 영향을 미치지 않는데(보이거나 않보이거나 할 뿐이지 오류가 발생하지는 않는다), 이것은 개발자와 디자이너의 협업 시스템에서 매우 중요한 부분으로, 개발자는 뷰모델에 비지니스 로직을 구현하고, 출력요소들을 프로퍼티로 만들고, 인터렉션을 할 수 있는 ICommand를 추가해 놓으면, 디자이너는 만들어져있는 뷰 모델을 기반으로 화면 구성, 데이터 연결, 사용자 인터렉션까지 테스트를 하면서 화면을 만들수 있게 된다.

 

2) ICommand 사용 :

사용자의 익터렉션의 처리를 ICommand를 호출해서 처리를 하는데, .Net Framework 4.0에서는 InvokeCommandAction를 사용하고, .Net Framework 4.5에는 아직 InvokeCommandAction을 사용할 수 없어서, Button에 컨트롤에 존재하는 Command 프로퍼티를 이용해서 ICommand를 호출하게 된다. 아마 조금 지나면 지원을 할 것이라고 생각되며, 현재는 이벤트를 뷰 모델과 연결하는 방법으로 사용하고 있다.

 

3) Binding 사용 :

Property base로 작업을 하기 때문에 Blend 4.0, Blend 5.0 preview, VS11의 디자인 타임에서 뷰 모델의 property들을 컨트롤에 property로 Binding하여 사용 할 수 있는데, 이 design time interactivity는 디자이너와 개발자 모두에게 편리한 개발 경험을 제공해 준다.

 

3. 밀착연결(Tightly coupled)

먼저 밀착연결된 프로그램 셈플을 보고, 이 프로그램을 느슨한연결 프로그램으로 변경해 보도록 하겠다.

예제는 모두 VS11, .Net Framework 4.5를 사용한다.

 

MainWindows.xaml

 

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <DataTemplate x:Key="DataTemplate1">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Id = "/>
                <TextBlock Text="{Binding Id}" Margin="0,0,10,0"/>
                <TextBlock Text="Age = "/>
                <TextBlock Text="{Binding Age}" Margin="0,0,10,0"/>
                <TextBlock Text="Name = "/>
                <TextBlock Text="{Binding Name}" Margin="0,0,10,0"/>
                <TextBlock Text="Sex = "/>
                <TextBlock Text="{Binding Sex}"/>
            </StackPanel>
        </DataTemplate>
    </Window.Resources>

    <Grid>
        <StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Sample MVVM Pattern in WPF"/>
            </StackPanel>

            <ListBox x:Name="lbPeople" ItemTemplate="{DynamicResource DataTemplate1}" />

            <StackPanel Orientation="Horizontal">
                <Button Content="Set Current" Click="Button_Click_1" />
                <Button Content="Add Person" Click="Button_Click_2" />
                <Button Content="Remove Person" Click="Button_Click_3" />
            </StackPanel>
           
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Id = "/>
                <TextBox x:Name="tbId" Margin="0,0,10,0" IsReadOnly="True"/>
                <TextBlock Text="Age = "/>
                <TextBox x:Name="tbAge" Margin="0,0,10,0" LostFocus="tbAge_LostFocus"/>
                <TextBlock Text="Name = "/>
                <TextBox x:Name="tbName" Margin="0,0,10,0" LostFocus="tbName_LostFocus"/>
                <TextBlock Text="Sex = "/>
                <CheckBox x:Name="cbSex" LostFocus="cbSex_LostFocus" />
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

MainWondows.xaml.cs

 

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        ObservableCollection<Person> People;

        Person CurrentPerson;

        public MainWindow()
        {
            InitializeComponent();

            initData();
        }

        private void initData()
        {
            People = new ObservableCollection<Person>
            {
                new Person{ Id=0 , Age=11 , Name="kaki104" , Sex=true},
                new Person{ Id=1 , Age=22 , Name="kakao" , Sex=false},
                new Person{ Id=2 , Age=33 , Name="kass" , Sex=true},
                new Person{ Id=3 , Age=44 , Name="kameo" , Sex=false},
                new Person{ Id=4 , Age=55 , Name="keroro" , Sex=true}
            };
            CurrentPerson = People.FirstOrDefault();

            lbPeople.ItemsSource = People;
            displayCurrentPerson();
        }

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            //Set Current
            CurrentPerson = lbPeople.SelectedItem as Person;
            displayCurrentPerson();
        }

        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            //Add Person
            CurrentPerson = new Person();
            CurrentPerson.Id = People.Max(p => p.Id) + 1;
            People.Add(CurrentPerson);
            displayCurrentPerson();
        }

        private void Button_Click_3(object sender, RoutedEventArgs e)
        {
            //Remove Person
            if (CurrentPerson != null && People.Count(p => p.Id == CurrentPerson.Id) > 0)
            {
                People.Remove(CurrentPerson);
                CurrentPerson = null;
                displayCurrentPerson();
            }
        }

        private void displayCurrentPerson()
        {
            if (CurrentPerson != null)
            {
                tbId.Text = CurrentPerson.Id.ToString();
                tbAge.Text = CurrentPerson.Age.ToString();
                tbName.Text = CurrentPerson.Name;
                cbSex.IsChecked = CurrentPerson.Sex;
            }
            else
            {
                tbId.Text = "";
                tbAge.Text = "";
                tbName.Text = "";
                cbSex.IsChecked = null;
            }
        }

        private void tbAge_LostFocus(object sender, RoutedEventArgs e)
        {
            CurrentPerson.Age = Convert.ToInt32(tbAge.Text);
        }

        private void tbName_LostFocus(object sender, RoutedEventArgs e)
        {
            CurrentPerson.Name = tbName.Text;
        }

        private void cbSex_LostFocus(object sender, RoutedEventArgs e)
        {
            CurrentPerson.Sex = (cbSex.IsChecked == null || cbSex.IsChecked == false) ? false:true;
        }
    }
}

 

위의 소스를 실행한 결과이다. 한줄을 선택 한 후 Set Current버튼을 누르면 편집을 할 수가 있으며, Add Person은 추가, Remove Person은 현재 편집 중인 데이터를 삭제한다.

 

1) MainWindows.xaml과 MainWindows.xaml.cs는 하나의 세트로 이렇게 비지니스 로직과 뷰가 하나로 묶여 있는 것을  Tightly coupled 방식이라고 하며, 아래는 디자인 타임 뷰 상태로 데이터는 보이지 않고, 이 상태로 디자인을 해야 한다.

 

2) CurrentPerson의 data를 TextBox에 입력하는 과정을 보면,

 

tbId.Text = CurrentPerson.Id.ToString();
tbAge.Text = CurrentPerson.Age.ToString();
tbName.Text = CurrentPerson.Name;
cbSex.IsChecked = CurrentPerson.Sex;


위와 같은 방식으로 입력을 하는데, 이 것은 object를 data로만 처리를 하는 것으로, CurrentPerson object가 가지고 있던 Age의 의미가 tbAge.Text로 이동하면서 그냥 숫자 11이란 의미로 변경이 되는 것이다. 또한, CurrentPerson이 변경이 될 때 마다 displayCurrentPerson()을 호출해서 화면에 새로 출력해 주어야 한다.

 

3) LostFocus 이벤트 처리를 보면,

private void tbAge_LostFocus(object sender, RoutedEventArgs e)
{
    CurrentPerson.Age = Convert.ToInt32(tbAge.Text);
}

private void tbName_LostFocus(object sender, RoutedEventArgs e)
{
    CurrentPerson.Name = tbName.Text;
}

private void cbSex_LostFocus(object sender, RoutedEventArgs e)
{
    CurrentPerson.Sex = (cbSex.IsChecked == null || cbSex.IsChecked == false) ? false:true;
}

각 컨트롤에 LostFocus 이벤트를 발생시키면 자신의 데이터를 다시 CurrentPerson의 각 프로퍼티로 이동을 시켜주면서, data를 object의 property에 입력을 해주게 된다.

 

4) button 처리 방식을 보면

private void Button_Click_1(object sender, RoutedEventArgs e)
{
    //Set Current
    CurrentPerson = lbPeople.SelectedItem as Person;
    displayCurrentPerson();
}

Set Current버튼을 클릭했을 때 lbPeople이란 ListBox 컨트롤을 알고 있어야 한다.

 

4. MVVM Pattern을 이용한 느슨한 연결(loosly coupled)

기존 솔루션에 WpfApplication2를 추가했다.

 

MainWindow.xaml

 

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ViewModels="clr-namespace:WpfApplication2.ViewModels"
        x:Class="WpfApplication2.MainWindow"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <DataTemplate x:Key="DataTemplate1">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Id = "/>
                <TextBlock Text="{Binding Id}" Margin="0,0,10,0"/>
                <TextBlock Text="Age = "/>
                <TextBlock Text="{Binding Age}" Margin="0,0,10,0"/>
                <TextBlock Text="Name = "/>
                <TextBlock Text="{Binding Name}" Margin="0,0,10,0"/>
                <TextBlock Text="Sex = "/>
                <TextBlock Text="{Binding Sex}"/>
            </StackPanel>
        </DataTemplate>
    </Window.Resources>

    <Window.DataContext>
        <ViewModels:MainWindowsViewModel/>
    </Window.DataContext>

    <Grid>
        <StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Sample MVVM Pattern loosly coupled in WPF"/>
            </StackPanel>
            <ListBox x:Name="lbPeople" ItemsSource="{Binding People}" ItemTemplate="{DynamicResource DataTemplate1}" />
            <StackPanel Orientation="Horizontal">
                <Button Content="Set Current" Command="{Binding SelectChangedCommand, Mode=OneWay}" CommandParameter="{Binding SelectedItem, ElementName=lbPeople}"/>
                <Button Content="Add Person" Command="{Binding AddCommand, Mode=OneWay}"/>
                <Button Content="Remove Person" Command="{Binding RemoveCommand, Mode=OneWay}" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Id = "/>
                <TextBox Text="{Binding CurrentPerson.Id}" Margin="0,0,10,0" IsReadOnly="True"/>
                <TextBlock Text="Age = "/>
                <TextBox Text="{Binding CurrentPerson.Age, Mode=TwoWay}" Margin="0,0,10,0"/>
                <TextBlock Text="Name = "/>
                <TextBox Text="{Binding CurrentPerson.Name, Mode=TwoWay}" Margin="0,0,10,0"/>
                <TextBlock Text="Sex = "/>
                <CheckBox IsChecked="{Binding CurrentPerson.Sex, Mode=TwoWay}"/>
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

MainWindow.xaml.cs

 

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

 

MainWindowViewModel.cs

 

public class MainWindowsViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    ObservableCollection<Person> people;
    public ObservableCollection<Person> People
    {
        get { return people; }
        set
        {
            people = value;
            RaisePropertyChanged("People");
        }
    }

    private Person currentPerson;
    public Person CurrentPerson
    {
        get { return currentPerson; }
        set
        {
            currentPerson = value;
            RaisePropertyChanged("CurrentPerson");
        }
    }

    public MainWindowsViewModel()
    {
        People = new ObservableCollection<Person>
        {
            new Person{ Id=0 , Age=11 , Name="kaki104" , Sex=true},
            new Person{ Id=1 , Age=22 , Name="kakao" , Sex=false},
            new Person{ Id=2 , Age=33 , Name="kass" , Sex=true},
            new Person{ Id=3 , Age=44 , Name="kameo" , Sex=false},
            new Person{ Id=4 , Age=55 , Name="keroro" , Sex=true}
        };
        CurrentPerson = People.FirstOrDefault();
    }

    ICommand addCommand;
    public ICommand AddCommand
    {
        get
        {
            if (addCommand == null)
            {
                addCommand = new DelegateCommand(obj =>
                {
                    CurrentPerson = new Person();
                    CurrentPerson.Id = People.Max(p => p.Id) + 1;
                    People.Add(CurrentPerson);
                });
            }
            return addCommand;
        }
    }

    ICommand removeCommand;
    public ICommand RemoveCommand
    {
        get
        {
            if (removeCommand == null)
            {
                removeCommand = new DelegateCommand(obj =>
                {
                    var person = obj as Person;
                    if (person != null && People.Count(p => p.Id == person.Id) > 0)
                    {
                        People.Remove(person);
                    }
                });
            }
            return removeCommand;
        }
    }

    ICommand selectChangedCommand;
    public ICommand SelectChangedCommand
    {
        get
        {
            if (selectChangedCommand == null)
            {
                selectChangedCommand = new DelegateCommand(obj =>
                {
                    var person = obj as Person;
                    if (person != null && People.Count(p => p.Id == person.Id) > 0)
                    {
                        CurrentPerson = person;
                    }
                });
            }
            return selectChangedCommand;
        }
    }

}

 

WpfApplication2를 실행한 결과이며, 결과 화면은 동일함을 알 수 있다. 이제 하나씩 차이점을 찾아보도록 하자.

 

1) MainWindows.xaml과 MainWindowsViewModel.cs이 DataContext로 연결이 되는 것을 loosly coupled 방식이라고 한다. MainWindowsViewModel은 디자인 타임에도 인스턴스가 되며, 인스턴스된 뷰 모델은 DataContext에 입력이 되고, 입력된 내용은 하위 컨트롤들의 DataContext에 상속된다.

 

    <Window.DataContext>
        <ViewModels:MainWindowsViewModel/>
    </Window.DataContext>

 

그렇다면, 디자인 타임에 어떻게 보이는지 확인해 보자

WpfApplication1의 디자인 타임화면과 너무나 다른 화면을 볼 수 있다. 디자인 타임, 런타임 화면이 완전히 동일하기 때문에 중간에 노란색선은 이 것이 디자인 타임이라는 것을 확인 시켜 주기 위해서 일부러 추가한 것으로, 데이터가 보이는 디자인 화면이라면 디자인을 하기도 무척 쉽다는 것을 알 수 있다.

 

2) CurrentPerson의 data를 출력하는 부분을 보면

 

<TextBlock Text="Id = "/>
<TextBox Text="{Binding CurrentPerson.Id}" Margin="0,0,10,0" IsReadOnly="True"/>
<TextBlock Text="Age = "/>
<TextBox Text="{Binding CurrentPerson.Age, Mode=TwoWay}" Margin="0,0,10,0"/>
<TextBlock Text="Name = "/>
<TextBox Text="{Binding CurrentPerson.Name, Mode=TwoWay}" Margin="0,0,10,0"/>
<TextBlock Text="Sex = "/>
<CheckBox IsChecked="{Binding CurrentPerson.Sex, Mode=TwoWay}"/>

위에서도 이야기를 했듯이, MVVM Pattern을 사용하면 property를 이용하기 때문에 이렇게 입력한 것만으로 화면에 데이터가 출력이되고, 수정된 데이터가 다시 뷰 모델의 프로퍼티로 입력이 된다.

 

그렇다면, 이런 코딩을 직접 입력해야 하는것인가? 그것도 아니다, VS11이나 Blend를 이용하면 인터렉티브하게 입력할 수 있다.

 

TextBox의 Text 프로퍼티 오른쪽에 네모를 클릭하고 팝업메뉴에서 Create Data Binding을 클릭하면 팝업창이 출력된다. 그곳에서 현재 Data context에 있는 프로퍼티, ICommand 목록을 볼 수 있고, 그 중에 하나를 선택하면 자동으로 코드가 입력된다.

 

3) button 처리

 

<Button Content="Set Current" Command="{Binding SelectChangedCommand, Mode=OneWay}" CommandParameter="{Binding SelectedItem, ElementName=lbPeople}"/>

 

버튼의 Command를 Binding 할때도 위와 같은 방식으로 처리하면 되고, 여기서는 CommandParameter를 보면 ICommand를 호출할 때 lbPeople의 SelectItem 프로퍼티를 파라메터로 넘기도록 되어 있다. 이 파라메터는

 

ICommand selectChangedCommand;
public ICommand SelectChangedCommand
{
    get
    {
        if (selectChangedCommand == null)
        {
            selectChangedCommand = new DelegateCommand(obj =>
            {
                var person = obj as Person;
                if (person != null && People.Count(p => p.Id == person.Id) > 0)
                {
                    CurrentPerson = person;
                }
            });
        }
        return selectChangedCommand;
    }
}

obj로 이곳에 전달이 되고, 그 obj를 Person으로 변경한 후 처리한다. 그러므로, 이 곳에서는 어떤 컨트롤로 부터 데이터가 들어오는지 알 필요가 없다.

 

5. 간단하게 MVVM Pattern을 사용하면 어떤 이점이 있는지 알아 보았다.

그러나, 이 포스트에서는 정말 심플한 경우에 대해서만 다루었으며, 실재로 대규모 프로젝트를 했을 때는 Prism 4.0을 사용하게 되는데 이때는 MVVM Pattern의 활용도가 더 높아지고, MEF, Rx를 추가해서 성능을 높이기 위해서는 거의 필수적으로 사용이 되어야 하는 패턴이다. 그리고, 기본 내용을 습득한 후에는 MVVM Pattern을 좀더 쉽게 적용하기 위해 GalaSoft MVVM Light, Caliburn.Micro 등의 NuGet Packages들을 활용하는 것도 좋은 방법이 될 수 있다. 추후 이런 부분에 대한 요청이 있으면, 추가 포스트를 하도록 하겠다.

 

6. 소스

 

MVVMPattern.zip

블로그 이미지

kaki104

/// Microsoft MVP - Windows Development - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

티스토리 툴바