[WPF] MVVM 패턴 이해하기 – 2

[WPF] MVVM 패턴 이해하기 – 2

이번 글에서는 지난 글에 이어 MVVM 패턴이 적용된 WPF 샘플 코드를 짜보겠습니다. 사람의 이름과 이름을 입력 한 뒤 리스트에 추가해주고, 리스트에서 항목을 선택하면 이름과 나이를 수정할 수 있는 프로그램을 만들어 보겠습니다.

우선 WPF 프로젝트를 하나 생성 한 뒤 이전 글에서 만들었던 Notifier와 Command 클래스를 추가합니다.

Notifier.cs에서 조금 달라진 것이 있다면 속성이 변경되는 것을 알리는 OnPropertyChanged 함수 인자에 CallerMemberName이 추가되었습니다. CallerMemberName은 호출한 메서드, 속성, 또는 이벤트의 이름을 자동으로 가져올 수 있게 해주는 특성입니다. 속성에서 OnPropertyChanged에 아무런 입력을 하지 않아도 해당 속성의 이름을 자동으로 인자로 넘기게 됩니다.

public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

MainWindow.xaml은 View, MainWindowViewModel.cs는 ViewModel, Model은 Person.cs로 할 예정입니다. MainWindowViewModel.cs와 Person.cs는 생성된 프로젝트에 없으므로 추가해줍니다.

View

먼저 View인 MainWindow.xaml에 아래와 같이 입력해줍니다.

<Window x:Class="WpfApp4.MainWindow"
        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:WpfApp4"
        xmlns:vm="clr-namespace:WpfApp4.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MainWindowViewModel />
    </Window.DataContext>
    <StackPanel Margin="10">
        <!-- 추가할 사람 정보 입력 -->
        <TextBlock Text="이름:" />
        <TextBox Text="{Binding AddName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
        <TextBlock Text="나이:" />
        <TextBox Text="{Binding AddAge, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
        <Button Content="Add" Command="{Binding AddCommand}" Margin="0,10,0,10"/>

        <Border Background="Gray" Height="3" Margin="10"/>

        <!-- 입력된 모든 사람 정보 표시 -->
        <ListBox ItemsSource="{Binding People}" SelectedItem="{Binding SelectedPerson, Mode=TwoWay}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="Name : "/>
                        <TextBlock Text="{Binding Name}" Margin="0,0,10,0"/>
                        <TextBlock Text="Age : "/>
                        <TextBlock Text="{Binding Age}"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <Border Background="Gray" Height="3" Margin="10"/>

        <!-- 리스트에서 선택된 SelectedPerson 정보 표시 -->
        <Border DataContext="{Binding SelectedPerson}">
            <StackPanel>
                <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
                <Slider Value="{Binding Age, Mode=TwoWay}" Maximum="100"/>
            </StackPanel>
        </Border>
    </StackPanel>
</Window>

크게 나누어 보면 정보 입력 화면, 입력된 전체 정보 표시 화면(리스트), 선택된 정보 수정 화면으로 구성되어 있습니다.

입력 화면에서는 AddName, AddAge, ADdCommand 속성을 주로 보시면 되고, 입력된 전체 정보를 표시하는 리스트에서는 People, SelectedPerson, Name, Age 속성을, 선택한 정보를 표시하는 화면에서는 SelectedPerson, Name, Age 속성을 보시면 됩니다.

ViewModel

위에서 언급한 속성을 연결하기 위해 MainWindowViewModel.cs에 아래와 같이 입력해줍니다.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using WpfApp4.Models;
using WpfApp4.Util;

namespace WpfApp4.ViewModels
{
    public class MainWindowViewModel : Notifier
    {
        private string _addName = "홍길동";
        public string AddName // 이름 입력
        {
            get => _addName;
            set
            {
                _addName = value;
                OnPropertyChanged();
                AddCommand.RaiseCanExecuteChanged();
            }
        }

        private int _addAge = 0;
        public int AddAge // 나이 입력
        {
            get => _addAge;
            set
            {
                _addAge = value;
                OnPropertyChanged();
                AddCommand.RaiseCanExecuteChanged();
            }
        }

        public Command AddCommand { get; } // Add 버튼 Command

        public ObservableCollection<Person> People { get; } // 리스트에 표시되는 전체 정보

        private Person _selectedPerson;
        public Person SelectedPerson // 리스트에서 선택한 정보
        {
            get => _selectedPerson;
            set
            {
                _selectedPerson = value;
                OnPropertyChanged();
            }
        }

        public MainWindowViewModel()
        {
            AddCommand = new Command(OnAdd, CanExecuteAdd);

            People = new ObservableCollection<Person>();
        }

        private void OnAdd()
        {
            Person person = new Person(AddName, AddAge);
            People.Add(person);
            AddName = "";
            AddAge = 0;
        }

        private bool CanExecuteAdd()
        {
            return !string.IsNullOrEmpty(AddName);
        }
    }
}

코드를 보시면 알겠지만 큰 어려움은 없습니다. 재물 파일에서 사용되는 속성들을 ViewModel에서 선언하여 연결해주면 됩니다.

Model

View에서 사용된 속성 중에 ViewModel에 정의되지 않은 속성이 있습니다. NameAge 속성이 ViewModel에 정의되어 있지 않은데, 이 두 속성은 Model인 Person.cs에 정의해주겠습니다.

public class Person
{
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
    public string Name { get; set; }
    public int Age { get; set; }
}

코드 자체는 크게 어렵지 않습니다.

실행

실제 실행하면 아래와 같이 동작합니다.


github : https://github.com/3001ssw/c_sharp/tree/main/WPF/Simple_WfpApp/WpfApp4