티스토리 뷰

프로그래밍

C# WPF MVVM 패턴 체험기

야라바 2025. 9. 24. 21:36
728x90

프로그램 개발 및 유지 보수의 생산성을 높이고, 협업으로 시너지를 높일 수 있는 아키텍처는 지속적으로 개발되어 왔는데 대표적인 예들은 아래와 같다.

  • MVC (Model, View, Controller)
  • MVP (Model, View, Presenter)
  • MVI (Model, View, Intent)
  • MVVM (Model, View, View Model)

이번에 다룰 MVVM은 MVC에서 파생된 것으로 차이점이라면 MVC 모델에서는 Controller가 입력을 받는다면 MVVM에서는 View에서 입력을 받는다. 무엇보다 WPF 응용에서 가장 널리 사용하는 프로그래밍 패턴이다. 물론 크로스플랫폼 UI인 아발론 UI에서도 MVVM을 채용하고 있다. MVVM은 각 요소를 독립적으로 개발 및 테스트를 진행할 수 있도록 만들어진 패턴이다. 본 포스팅에서는 "C# WPF 첫 프로그램 만들기"에서 수행한 WPF 프로젝트를 바탕으로 진행하고자 한다.

 

통상 MVVM 패턴으로 프로젝트를 개발하는 경우에는 프로젝트 하위 폴더로 Models, Views, ViewModels 폴더들을 만들고 각각의 성격에 따라 파일을 저장하도록 가이드하고 있지만 본 포스팅은 간단한 맛보기를 수행할 예정이므로 폴더 생성 작업은 생략하고 진행한다. 작은 규모의 프로젝트이더라도 이렇게 폴더 구조를 만들고 시작하는 것이 추후 개발 피드백이나 유지 보수를 위해서도 좋겠다는 생각이 든다.


■ 모델 생성

MVVM에서 모델(Models)은 응용 프로그램의 데이터와 데이터를 기반으로 한 비즈니스 로직을 수행한다. 모델은 뷰나 뷰모델에 대한 의존성이 전혀 없다. 모델 입장에서는 뷰에 대해서도 뷰모델에 대해서도 접근 경로가 없다.

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfApp1
{
    public class Person
    {
        public string Name { get; set; }

        public int Age { get; set; }
    }
}

 

본 포스팅에서는 위의 코드처럼 이름과 나이를 가진 Person이라는 아주 단순한 모델을 구현하고자 한다. 프로젝트를 우측 마우스로 클릭하여 콘텍스트 메뉴에서 추가> 클래스를 선택하여 ModelPerson.cs이라는 코드를 추가한다. 

 

INotifyPropertyChanged 인터페이스를 통해서  PropertyChanged 이벤트를 발생시키는 방법으로 속성 값이 바뀌었을 때 해당 속성을 바인딩하고 있는 UI 요소로 하여금 변경 내역을 반영하도록 할 수 있지만 본 예제에서는 적용하지 않았다.


■ 뷰 생성

뷰는 프로그램의 사용자 인터페이스(UI)를 표현하며 XAML로 형태로 작성된다. 앞선 예제 화면을 수정하는 방식으로 수행한다. 따라 하려면 디자이너 모드에서 수정하기보다는 XAML을 붙여 넣기 하는 방법이 쉽다.

 

Winform과는 다르지만 위의 그림처럼 디자이너 모드에서 작성할 수도 있지만 XAML 파일을 직접 수정할 수도 있다. 본 예제에서는 상단에 텍스트 박스를 두고, 중간의 리스트뷰를 선택하지 않은 상태에서는 [추가] 버튼으로 항목을 추가할 수 있도록 한다. 리스트 뷰의 특정 항목을 선택하면 해당 내용이 텍스트 박스에 출력되고, 텍스트 박스 내용을 수정하면 리스트 뷰에 반영되는 방식이다. 맨 하단의 [저장]을 누르면 최종 리스트 뷰 내용을 정리해서 대화창으로 보여준다.

 

<Window x:Class="WpfApp1.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:viewModel="clr-namespace:WpfApp1"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <viewModel:MainViewModel/>
    </Window.DataContext>
    <Grid Margin="0,0,0,20">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <StackPanel Margin="20" Grid.Row="0">
            <TextBlock Text="이름 :"/>
            <TextBox Name="txtName" Text="{Binding ElementName=UserGrid, Path=SelectedItem.Name}" Width="200" HorizontalAlignment="Left"/>
            <TextBlock Text="나이 :"/>
            <TextBox Name="txtAge" Text="{Binding ElementName=UserGrid, Path=SelectedItem.Age}" Width="200" HorizontalAlignment="Left"/>
            <Button Content="추가" Height="23" Width="141" Name="btnNew" Command="{Binding Path=AddCommand}" CommandParameter="{Binding Text, ElementName=txtName}"/>
        </StackPanel>
        <ListView Name="UserGrid" Grid.Row="1" ItemsSource="{Binding Users}">
            <ListView.View>
                <GridView x:Name="grdTest">
                    <GridViewColumn Header="이름" DisplayMemberBinding="{Binding Name}" Width="50"/>
                    <GridViewColumn Header="나이" DisplayMemberBinding="{Binding Age}" Width="80"/>
                </GridView>
            </ListView.View>
        </ListView>
        <StackPanel Margin="20" Grid.Row="2">
            <Button Content="저장" Height="23" Width="141" Name="btnUpdate" Command="{Binding Path=UpdateCommand}" />
        </StackPanel>
    </Grid>
</Window>

 

뷰는 뷰모델과 데이터 바인딩을 통해 연결된다.  먼저 사용할 뷰모델을 지정해야 하는데  먼저 xmlns:viewModel="clr-namespace:WpfApp1" 문장에 뷰가 참조할 뷰모델의 네임스페이스를 지정한다. 필자는 뷰모델이 하나인 간단한 예제이므로 메인 프로그램과 동일한 네임스페이스를 지정했으나 뷰모델 별로 별도의 네임스페이스를 지정하는 것도 방법이다. 뷰모델 지정을 위한 또 다른 구문이 있는데 Window.DataContext 태그에 viewModel 속성으로 사용할 뷰모델 클래스를 기술해야 한다. 물론 이러한 과정은 XAML이 아니라 *. cs 코드에서도 지정할 수 있다.

 

코드 내의 데이터 바인딩 과정을 보면 txtName, txtAge의 텍스트 박스는 뷰모델과 연동된 것이 아니라 뷰 내부의 다른 요소인 UserGrid 리스트뷰를 보고 있고 선택된 현재 행의 특정 항목을 지정하고 있다. 리스트뷰는 ItemsSource="{Binding Users}"과 같이 뷰모델을 바인딩했다. 뷰모델의 데이터 속성뿐만 아니라 버튼을 보면 Command="{Binding Path=AddCommand}"와 같은 방식으로 명령 수행 루틴을 연결하고 있다.

 

■ 뷰모델 생성

뷰모델은 뷰가 필요한 모델의 데이터를 가공해 주거나 필요한 명령을 제공하는 방식으로 뷰와 모델 사이를 연결해 주는 역할을 한다. 프로젝트를 우측 마우스로 클릭하여 콘텍스트 메뉴에서 추가> 클래스를 선택하여 MainViewModel.cs.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.Input;

namespace WpfApp1
{
    class MainViewModel
    {
        public MainViewModel()
        {
            Users = new ObservableCollection<Person>();
            Users.Add(new Person { Name = "홍길동", Age = 40 });
            Users.Add(new Person { Name = "홍길순", Age = 30 });
            Users.Add(new Person { Name = "김개똥", Age = 45 });
            UpdateCommand = new RelayCommand(UpdatePerson);
            AddCommand = new RelayCommand(AddPerson, canAddPerson);
        }

        public ObservableCollection<Person> Users { get; set; }

        public ICommand UpdateCommand { get; private set; }

        private void UpdatePerson(object parameter)
        {
            string rst = "";

            foreach (var itm in Users)
            {
                if (rst != "") rst += "\n";
                rst += itm.Name + "=" + itm.Age;
            }
            System.Windows.MessageBox.Show(rst);
        }
        public ICommand AddCommand { get; private set; }

        private void AddPerson(object param)
        {
            Users.Add(new Person { Name = (string)param, Age = 20 });
        }

        private bool canAddPerson(object param)
        {
            if (param == null) return false;
            if (((string)param).Length <= 0) return false;
            foreach (var itm in Users)
            {
                if (itm.Name == (string)param) return false;
            }

            return true;
        }
    }

    public class RelayCommand : ICommand
    {
        private readonly Action<object> _execute;
        private readonly Predicate<object> _canExecute;

        public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
        {
            _execute = execute ?? throw new ArgumentNullException(nameof(execute));
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true;

        public void Execute(object parameter) => _execute(parameter);

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    }
}

 

리스트뷰에서 바인딩하는 Users 리스트를 ObservableCollection으로 정의했는데, List가 아니라 ObservableCollection을 사용하면 뷰모델에서 내용을 추가하면 뷰에 바로 적용한다. ICommand 인터페이스를 구현한 RelayCommand 클래스를 사용하면 편리하게 뷰에서 필요한 명령을 만들 수 있다. 

 

위의 예제에서는 Update는 CanExecute Predicate를 적용하지 않았지만 Add는 적용했는데 뷰에서 CommandParameter="{Binding Text, ElementName=txtName}" 방식으로 지정하면 자동적으로 CanExecute를 호출해서 명령 수행 가능 여부를 전달할 수 있다. 예제에서는 내용이 입력되고 기존에 존재하지 않는 이름일 때 Add를 수행할 수 있고 나머지는 수행할 수 없도록 전달하고 있다.

 

이렇게 간단한 예제를 풀고 나니 MVVM 패턴이 복잡한 것으로 끝나는 것이 아니구나 하는 생각이 든다. 

 

 

728x90
댓글
최근에 올라온 글
최근에 달린 댓글
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
글 보관함