본문 바로가기

Programming/Laboratory

WPF, MVVM | Calibrun.Micro와 C#으로 WPF 애플리케이션 만들기 (2)


 이번에는 Caliburn.Micro를 사용해서 간단한 프로그램을 작성해보겠습니다. 공식 문서에는 따로 Model에 대한 설명이 없어 Model 없이 View와 ViewModel로만 프로그램 작성을 해야 하는가 조금 헷갈릴 수도 있지만, Model 역시 사용 가능합니다.

 

 예를 들어, 학생부 프로그램을 작성하기 위해 'Student'에 대한 Model이 필요하다면

namespace MVVMDemo.Models
{
    public class StudentModel
    {
    	public BitmapImage Picture { get; set; } // 학생 사진
        public string Grade { get; set; } // 학년
        public string Name { get; set; } // 이름
        public string Address { get; set; } // 주소
        ...
    }
}

 다음과 같은 방식으로 적어서 ObservableCollection을 상속받은 BindableCollection으로 바인딩된 컨트롤과 Model을 안정적으로 동기화할 수 있게 해 줍니다.

 

 그럼 자세히 알아보도록 하겠습니다.

 

Model & Property


 앞서 예시로 말했던 간단한 학생부 프로그램을 작성해보겠습니다. 학생은 '이름'과 '학년'이라는 속성을 가지고 있다고 하겠습니다. 프로젝트는 앞서 제작했던 프로젝트에서 계속 진행하겠습니다.

 

개발환경 - Visual Studio 2017

 

01. 'Models' 폴더에 'StudentModel.cs'를 추가하고 다음과 같이 입력합니다.

// Models/StudentModel.cs
namespace MVVMDemo.Models
{
    public class StudentModel
    {
        public string Name { get; set; }
        public string Grade { get; set; }
    }
}

 

02. 'ViewModels' 폴더의 'ShellViewModel.cs'를 다음과 같이 수정합니다.

// ViewModels/ShellViewModels.cs
using Caliburn.Micro;
using MVVMDemo.Models;

namespace MVVMDemo.ViewModels
{
    public class ShellViewModel : Screen
    {
        private BindableCollection<StudentModel> _students = new BindableCollection<StudentModel>();
        public BindableCollection<StudentModel> Students
        {
            get { return _students; }
            set { _students = value; }
        }

        private StudentModel _selectedStudent;
        public StudentModel SelectedStudent
        {
            get { return _selectedStudent; }
            set
            {
                _selectedStudent = value;
                NotifyOfPropertyChange(() => _selectedStudent);
            }
        }
    }
}

 BindableCollection은 객체의 속성 값이 변경될 때마다 외부에 알리는 역할을 합니다. StudentModel의 변경(추가, 제거, 이동)이 일어나게 되면 NotifyOfPropertyChange를 호출하여 값이 변경되었음을 외부에 알리게 됩니다.

 

  예를 들어, TextBox와 SelectedStudent가 바인딩이 된 상태일 경우,

      1. TextBox의 값이 변경
      2. SelectedStudent의 NotifyOfPropertyChange 호출
      3. _selectedStudent에게 알림

  다음과 같은 순서로 작동하게 됩니다.

 

 

03. 계속해서 'ShellViewModel.cs'에 다음을 추가해줍니다.

public ShellViewModel()
{
    Students.Add(new StudentModel { Name = "James", Grade = "3rd" });
    Students.Add(new StudentModel { Name = "Tony", Grade = "1st" });
    Students.Add(new StudentModel { Name = "Mike", Grade = "2nd" });
}

 'ShellViewModel.cs'의 전체 구조는 다음과 같습니다.

// ViewModels/ShellViewModels.cs
using Caliburn.Micro;
using MVVMDemo.Models;

namespace MVVMDemo.ViewModels
{
    public class ShellViewModel : Screen
    {
        public ShellViewModel()
        {
            Students.Add(new StudentModel { Name = "James", Grade = "3rd" });
            Students.Add(new StudentModel { Name = "Tony", Grade = "1st" });
            Students.Add(new StudentModel { Name = "Mike", Grade = "2nd" });
        }

        private BindableCollection<StudentModel> _students = new BindableCollection<StudentModel>();
        public BindableCollection<StudentModel> Students
        {
            get { return _students; }
            set { _students = value; }
        }

        private StudentModel _selectedStudent;
        public StudentModel SelectedStudent
        {
            get { return _selectedStudent; }
            set
            {
                _selectedStudent = value;
                NotifyOfPropertyChange(() => _selectedStudent);
            }
        }
    }
}

 데이터베이스를 사용한다면 StudentModel을 자동으로 채우는 로직을 짜면 되겠지만, 간편한 프로그램 작성을 목표로 하고 있으므로 일단 예시로 3개의 값을 하드코딩합니다.

 

04. 'Views' 폴더의 'ShellView.xaml'을 다음과 같이 수정합니다.

// Views/ShellView.xaml
<Window x:Class="MVVMDemo.Views.ShellView"
        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:MVVMDemo.Views"
        mc:Ignorable="d"
        Title="ShellView" 
        Height="500"
        Width="500"
        FontSize="20">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="7*"/>
            <ColumnDefinition Width="3*"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="1*"/>
            <RowDefinition Height="2*"/>
            <RowDefinition Height="4*"/>
            <RowDefinition Height="2*"/>
            <RowDefinition Height="2*"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>

        <ListView
            Grid.Row="1"
            Grid.Column="1"
            Grid.RowSpan="2"
            Grid.ColumnSpan="2"
            Margin="10"
            x:Name="StudentList"
            ItemsSource="{Binding Students}"
            SelectedItem="{Binding SelectedStudent}">
            <ListView.View>
                <GridView>
                    <GridViewColumn 
                        Width="280"
                        Header="Name"
                        DisplayMemberBinding="{Binding Name}"/>
                    <GridViewColumn
                        Width="110"
                        Header="Grade"
                        DisplayMemberBinding="{Binding Grade}"/>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

 

 

  ItemsSource="{Binding Students}"
      ‣ ItemsSource 속성을 BindableCollection인 Students와 바인딩한다.

  SelectedItem="{Binding SelectedStudent}"
      ‣ ListView 목록 중에 선택된 값을 NotifyOfPropertyChange를 통해 _selectedStudent에게 전달

  DisplayMemberBinding="{Binding Name}"
      ‣ Students의 Name을 바인딩

 

 작성이 완료되었으면 기다란 ListView 하나 있습니다.

 그리고 F5를 눌러 실행하면...!

 

 

 바인딩 했던 Students의 Name과 Grade가 ListView에 전부 나타났습니다. 만약 TextBlock의 Text나 Button의 Content를 바꾸고 싶다면 아래처럼

// TextBlock Binding
<TextBlock Text="{Binding Name}"/>

// Button Binding
<Button Content="{Binding Name}"/>

 이런 식으로 적으면 자연스럽게 바인딩이 됩니다.

 

Method & Event


 다음으로는 Method와 Event를 처리하는 방법에 대해서 알아보겠습니다. 원칙적으로 View와 ViewModel은 서로 의존성이 없기 때문에 기존 WPF에서 제공하는 방식으로 Event를 처리하면 작동을 하지 않습니다. 때문에 Event를 처리하기 위해서는 Caliburn.Micro가 제공하는 조금 다른 방식을 사용해야 합니다.

 

 이번에는 학생의 이름, 학생의 학년을 입력해서 업데이트를 하는 기능을 만들어보겠습니다.

 

01. 'ViewModels' 폴더의 'ShellViewModel.cs'에 다음 문구를 추가합니다.

// Views/ShellView.xaml

...

        <TextBox
            Grid.Row="3"
            Grid.Column="1"
            Margin="10"
            x:Name="txbName"
            Text="{Binding UpdateName}"/>

        <ComboBox
            Grid.Row="3"
            Grid.Column="2"
            Margin="10" 
            IsReadOnly="True"
            x:Name="GradeList"
            SelectedItem="{Binding UpdateGrade}"/>

        <Button
            Grid.Row="4"
            Grid.Column="2"
            Margin="10"
            x:Name="btnUpdate"
            Content="Update"/>

...

 

 디자인 부분은 다음과 같이 변경되어 있으면 됩니다.

 

 

02. 'ViewModels' 폴더의 'ShellViewModel.cs'에 다음 문구를 추가합니다.

// ViewModels/ShellViewModel.cs

...

using System.Collections.Genenric;

...

        private string _updateName;
        public string UpdateName
        {
            get { return _updateName; }
            set { _updateName = value; }
        }

        // ComboBox의 ComboBoxItem에 들어갈 List
        public List<string> GradeList
        {
            get
            {
                return new List<string> { "1st", "2nd", "3rd" };
            }
        }

        private string _updateGrade;
        public string UpdateGrade
        {
            get { return _updateGrade; }
            set
            {
                _updateGrade = value;
                NotifyOfPropertyChange(() => _updateGrade);
            }
        }

        public void btnUpdate()
        {
            string name = UpdateName;
            string grade = UpdateGrade;
            Students.Add(new StudentModel { Name = name, Grade = grade });
        }
        
...

 전부 적고 F5를 눌러 실행시킨 뒤, TextBox에 이름을 적고 ComboBox에서 학년을 선택한 뒤 Update 버튼을 눌러봅니다. 그러면...

 

 업데이트가 잘 작동합니다!

 

 얼핏보면 이게 왜 작동이 되는걸까 싶은데 View와 ViewModel을 같이 보면 이해가 되실 겁니다.

 

 

 TextBox의 바인딩은 위에서 설명한 것처럼 프로퍼티 이름을 적으면 바인딩이 됩니다.

 ComboBox는 List<>의 형식인 프로퍼티를 만들고, x:Name에 프로퍼티 이름을 적으면 ComboBoxItem으로 바인딩 됩니다. 그리고 ComboBox에서 선택 이벤트가 발행하면 선택된 아이템은 NotifyOfPropertyChange를 호출해 _updateGrade에 알립니다.

 Button은 x:Name과 메소드 이름이 동일하면 자동으로 Click 이벤트를 바인딩해 줍니다. 그리고 TextBox와 ComboBox에 있는 UpdateName과 UpdateGrade를 받아와서 Students라는 BindabledCollection을 하나 추가하는 겁니다.


 그런데 만약 버튼의 속성이나 EventArgs가 필요할 때는 어떻게 해야할까요?

 Caliburn.Micro는 'Message.Attach'를 통해 Event를 코드 비하인드에서 처리하는 것과 동일하게 사용할 수 있는 기능을 제공합니다.

 그러면, 'Message.Attach'를 이용해 Button의 Content를 MessageBox로 출력하는 기능을 만들어 보겠습니다.

 

01. 'Views' 폴더의 'ShellView.xaml'에서

        Window에 'xmlns:cal="http://www.caliburnproject.org"'

        Button에 'cal:Message.Attach="[Event PreviewMouseUp] = [Action btnUpdate_Clicked($source, $eventArgs)]"'

를 추가합니다.

// Views/ShellView.xaml
<Window x:Class="MVVMDemo.Views.ShellView"
        ...
        xmlns:cal="http://www.caliburnproject.org"
        ...        
        FontSize="20">
        
        ...
        
        <Button
            Grid.Row="4"
            ...
            Content="Update"
            cal:Message.Attach="[Event PreviewMouseUp] = [Action btnUpdate_Clicked($source, $eventArgs)]"/>
        
        ...

 

02. 'ViewModels' 폴더의 'ShellViewModel.cs'에서 'btnUpdate' 메소드를 지우고 다음과 같이 입력합니다.

// ViewModels/ShellViewModel.cs

...

        public void btnUpdate_Clicked(object sender, MouseButtonEventArgs e)
        {
            Button button = sender as Button;

            string buttonContent = button.Content.ToString();
            string name = UpdateName;
            string grade = UpdateGrade;

            MessageBox.Show(buttonContent);

            Students.Add(new StudentModel { Name = name, Grade = grade });
        }
        
...

 그리고 F5를 눌러 실행시킨 뒤, Update 버튼을 눌러봅니다.

 

 Button의 Content가 그대로 MessageBox에 출력됩니다!

 'ShellView.xaml'로 가서 Button의 Content를 다른 문자로 바꾸고 실행하면 바뀐 문자 그대로 반영되어 MessageBox가 나타날겁니다.


 오늘은 Caliburn.Micro의 기본적인 부분인 Model, Property, Method, Event에 대해 알아봤습니다. 실제로 사용해보면서 가볍고 간편하다는 생각이 드셨으면 좋겠습니다. 혹여나 그렇지 않다면 설명이 부족한 제 탓인거죠.

 따라하시면서 잘 모르겠다고 생각드는 부분은 바로 질문해주시면 제가 아는 선에서 최대한으로 답변해 드리겠습니다.

 

 다음에는 ViewModel과 ViewModel 사이에 값을 전달하는 EventAggregator에 대해 알아보겠습니다.