[C#/WPF] 간단한 데이터 바인딩 예제

웹페이지 만들면서 느꼈던 거지만 디자인패턴은 이해하기가 정말 힘든것 같다 (…..)

지난 겨울방학때 만들었던 홈페이지는 Code Igniter를 이용해서 MVC(Model-View-Controller)를 대충 봤는데 (사실 RoR로 계획하고 있었는데, RoR도 MVC였는데…?)

여튼 이번에 갑작스레 WPF를 접하게 되어서 연구실 형에게 들어보니 데이터바인딩을 해보면 신세계라고 해서 우와 나도 해봐야겠다 해서 해봤는데

아무리 생각해도 이건 윈폼에서 하던거랑 별반 다를게 없어서 조금 더 알아보고 했더니 클래스만 적당히 C# 코드로 만들어놓고 XAML에서 요리조리 하니까 따로 이벤트 핸들러 만들고 할 필요도 없는 것 같다. 여기에서 사용되는 패턴이 MVVM(Model-View-Viewmodel) 패턴이라고 한다.

자세한 건 모르겠는데, MVC와 대충 비슷한 것 같다. (….) 이번 학기에 디자인패턴에 대해서 배우니 그때 배울까…?

TextBox에 입력해서 Label의 Contents 변경하기

일단 뷰모델을 정의해야한다. 뷰모델을 정의할 때 가장 중요한 것은 프로퍼티다. 프로퍼티는 추후 XAML에서 바인딩되는 대상이 된다.

밑에서 사용하는 프로젝트 이름은 BindingTest이다. (WPF Project)

일단 다음과 같은 클래스를 정의한다.
[code language=”csharp” title=”Model/MyContents.cs”]
namespace BindingTest.Model
{
public class MyContents
{
private string contents;
public string Contents
{
get { return content; }
set
{
content = value;
}
}
}
}
[/code]

그러나 여기에는 문제가 있다. 이러한 모델을 정의한다고 해서, View 들의 내용을 동기화해주지는 않는다. 이러한 문제를 알아보기 위해 일단 XAML을 이용해서 TextBox의 Text와 Label의 Content를 위에서 만든 클래스와 바인딩해보자.

기초적인 내용이지만 클래스 자체로는 객체가 아니다. 클래스는 그저 설계도에 지나지 않기 때문에, 객체를 만들어야한다. 이러한 객체를 만들기 위해 MainWindow.xaml 파일을 다음과 같이 작성한다.

[code language=”xml” autolink=”false” title=”MainWindow.xaml” highlight=”7,10,11,12,26,27,28,30,31″]
<Window x:Class="BindingTest.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:BindingTest"
xmlns:c="clr-namespace:BindingTest.Model"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<c:MyContents x:Key="MyContentKey" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.DataContext>
<Binding Source="{StaticResource MyContentKey}" />
</Grid.DataContext>

<Label Margin="5,5" Name="MyLabel" Content="{Binding Path=Contents}"/>
<TextBox Margin="5,5" Name="MyText" Grid.Column="1" Text="{Binding Path=Contents}" />
<Button Margin="5,5" Grid.Column="2">Test</Button>
</Grid>
</Grid>
</Window>
[/code]

  • 외부 네임스페이스 import

    [code language=”xml” gutter=”false”]
    xmlns:c="clr-namespace:BindingTest.Model"
    [/code]
    C# 코드로 따진다면 다음과 같은 코드라고 볼 수 있다.
    [code language=”csharp” gutter=”false”]
    using c = BindingTest.Model;
    [/code]

  • MyContent 객체를 만들고 ResourceDictionary에 등록

    [code language=”xml” gutter=”false”]
    <Window.Resources>
    <c:MyContents x:Key="MyContentKey" />
    </Window.Resources>
    [/code]
    이는 C# 코드로 나타낸다면 다음과 같다.
    [code language=”csharp” gutter=”false”]
    this.Resources.Add("MyContentKey", new MyContent());
    [/code]
    여기서 ‘this’는 MainWindow 객체를 말한다.

  • 현재 Grid와 만들어 둔 MyContent 객체를 Binding

    [code language=”xml” gutter=”false”]
    <Grid.DataContext>
    <Binding Source="{StaticResource MyContentKey}" />
    </Grid.DataContext>
    [/code]

    위 코드는 StaticResource에서 Key가 MyContentKey인 리소스를 찾아서, 현재 Grid에 바인딩한다.

  • 바인딩 된 MyContent 객체의 프로퍼티와 TextBox, Label의 프로퍼티를 연결

    [code language=”xml” gutter=”false”]
    <Label Margin="5,5" Name="MyLabel" Content="{Binding Path=Contents}"/>
    <TextBox Margin="5,5" Name="MyText" Grid.Column="1" Text="{Binding Path=Contents}" />
    [/code]
    {Binding Path=Contents}는 현재 바인딩 된 객체에서 Contents라는 프로퍼티(뿐 아니라 Contents라는 이름으로 접근 가능한 멤버)와 현재 프로퍼티(TextBox.Text, Label.Content)를 연결한다.

그러나 위에서 말했듯이, 이렇게 바인딩만 해두었을 경우, 텍스트박스에 뭘 입력하든 레이블은 아무런 변화가 없다. 왜냐면 저 상태로는 MyContents의 Contents가 텍스트박스에 의해 set되어도 레이블은 Contents가 set되어서 변경이 되었는지 안되었는지 모르기 때문이다. 따라서 새로운 이벤트핸들러를 이용해서 프로퍼티가 변경 되었는지 안되었는지를 따로 알려주어야한다.

이러한 작용을 하기 위해서는 System.ComponentModel.INotifyPropertyChanged라는 인터페이스를 상속하면 된다.
이 인터페이스에는 딱 하나의 이벤트 맴버만 존재한다.

[code language=”csharp”]
interface System.ComponentModel.INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
[/code]

이벤트의 이름에서 알 수 있듯이, 해당 이벤트는 프로퍼티가 변경되면 일어난다. (혹은 일어나게 만들것이다.)

그래서 위의 MyContents 클래스를 다음과 같이 수정한다.
[code language=”csharp” highlight=”8,16,20,21,22,23,24,25″]
using System.ComponentModel;

namespace BindingTest.Model
{
public class MyContents : INotifyPropertyChanged
{
private string contents;
public event PropertyChangedEventHandler PropertyChanged;

public string Contents
{
get { return content; }
set
{
content = value;
OnPropertyChanged("Contents");
}
}

protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if(handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
[/code]

INotifyPropertyChanged를 상속하는 객체를 바인딩을 하게되면 PropertyChanged에 해당 View의 Content나 Text에 Contents를 대입하는 핸들러를 등록하는 것 같다.
따라서 Contents가 변경되면(set) OnPropertyChanged를 호출해서 “Contents”라는 이름으로 등록 된 프로퍼티의 PropertyChanged에 등록된 핸들러들을 모두 실행한다.(= 바인딩 된 객체들의 Content나 Text속성을 갱신한다)

그런데 문제는 TextBox의 경우 입력을 끝냈다는 것을 해당 텍스트박스가 Focus를 잃는 것으로 판단한다. 따라서 TextBox에 입력을 했다고 해서 바로 갱신이 되는 것이 아니라, Focus를 잃게 해야지 레이블이 갱신이 된다.

TextBox와 Slider 상호작용

은 귀찮아서 코드만

[code language=”csharp” title=”Model/MySliderValue.cs”]
using System;
using System.ComponentModel;

namespace BindingTest.Model
{
public class MySliderValue : INotifyPropertyChanged
{
private int value;
public event PropertyChangedEventHandler PropertyChanged;

public double Value
{
get { return value; }
set
{
this.value = Convert.ToInt32(value);
OnPropertyChanged("ValueString");
}
}
public string ValueString
{
get { return value.ToString(); }
set
{
this.value = Convert.ToInt32(value);
OnPropertyChanged("Value");
}
}
}
}
[/code]

[code language=”xml” title=”MainWindow.xaml”]

<Grid>
<Grid.Resources>
<c:MySliderValue x:Key="MySliderKey" />
</Grid.Resources>
<Grid.DataContext>
<Binding Source="{StaticResource MySliderKey}" />
</Grid.DataContext>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="*" />
<Grid.ColumnDefinitions>
<Slider Margin="5,5" Minimum="0" Maximum="100" Value="{Binding Path=Value}" />
<TextBox Margin="5,5" Grid.Column="1" Text="{Binding Path=ValueString}" />
</Grid>

[/code]

위에서 Value와 ValueString을 설정할 때 서로의 이벤트를 작동시키는 것은 슬라이더를 옮기면 텍스트박스의 Text를 변경해야하고, 텍스트박스의 Text를 변경시키면 슬라이더의 Value를 변경시켜야 하기 때문이다. 저건 때에따라서 다르게 해야할듯.

바인딩 모드

위에서 보면 객체가 존재하고, 뷰가 존재해서 뷰가 보여주는 정보는 객체의 정보고, 뷰에서 수정하면 객체의 정보도 수정되는 형식이다.
근데 이 방법에도 여러가지가 있다. 이것은 바인딩 시 Mode라는 어트리뷰트? 로 설정한다.

  1. OneWay
    OneWay는 일방적인 방법이다. 이것으로는 뷰에서 수정해도 객체의 정보는 수정되지 않는다. 오로지 객체의 정보를 불러오는(get) 일만 한다.
  2. TwoWay
    TextBox와 같은 것의 Default로 설정되어 있는 모드다. 얘는 양방향으로 컨트롤한다.
  3. OneWayToSource
    OneWay와는 반대로 정보를 수정하는 일(set)만 한다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다