본문 바로가기

.Net Technology/WPF

(11) 컨트롤 명령과 데이터 바인딩의 이해

WPF 컨트롤 명령의 이해

이번에는 명령(command)에 대해서 살펴보도록 하자. WPF는 컨트롤 명령을 이용한 신기한 이벤트 모델을 지원해주고 있다. 알다시피 닷넷의 이벤트는 기본 클래스에 정의되어 있고 클래스나 클래스로부터 파생되어 작성될 수 있다. 또한 닷넷 이벤트는 클래스들과 강하게 결합되어 있다.

반면에 WPF 컨트롤 명령은 이벤트처럼 특정 컨트롤에 지정하긴 하지만 다양한 형태로 여러 컨트롤들에게 지정하는 것이 가능하다. 예를 들어 WPF는 Copy, Paste, Cut과 같은 명령들을 지원하고 있고 이러한 명령을 여러 UI 개체들에게 지정해줄 수 있다는 것이다. 또한 Ctrl+C, Ctrl+V와 같은 키도드 단축키와 같은 명령을 이용할 수도 있다. 

윈도우 폼과 같은 다른 UI 프로그램에서 역시 이러한 이벤트를 만들 수도 있지만 많은 코드들을 작성해주어야 한다. 반면에 WPF에서는 이러한 명령들을 사용할 수 있고 사용자들은 쉽게 명령들을 설정할 수 있게 해준다.

기본적으로 지원하는 Command 객체

WPF는 여러 개의 컨트롤 명령을 지원하고 있고 이 컨트롤 명령들은 키보드 단축키와 같은 기능을 설정하는 것이라고 쉽게 생각하면 된다. 프로그램 관점으로 보자면 WPF 컨트롤 명령은 ICommand 인터페이스를 상속 받아서 특정 속성을 구현한 Command 클래스라고 보면 된다. 다음 코드를 살펴보자.

public interface ICommand
{
    // 커맨드 실행여부에 대한 변경이 일어났을 때 발생한다.
    event EventHandler CanExecuteChanged;
    // 현재 실행할 수 있는지에 대한 상태를 정의한다. 
    bool CanExecute(object parameter);
    // 명령에 의해서 실제 동작이 실행된다.
    void Execute(object parameter);
}


이 인터페이스를 이용해서 커맨드 명령을 직접 구현할 수 있지만 그전에 WPF에서 제공하고 있는 5가지의 명령들의 기능에 대해서 먼저 살펴보도록 하겠다. 여기서 소개할 이 클래스들은 스태틱 클래스로 ICommand를 상속 받아서 구현하였고 이벤트 라우팅을 위해서 RoutedUICommand를 구현하였다.

다음 [표29-4]는 WPF에서 기본적으로 몇 가지 속성들을 보여주고 있다. 

WPF 컨트롤 명령

속성

설명

ApplicationCommands

Close, Copy, Cut, Delete, Find, Open, Paste, Save, SaveAll, Redo, Undo

애플리케이션 단에서 사용되는 명령들을 정의하고 있다

ComponentsCommands

MoveDown, MoveFocusBack, MoveLeft, MoveRight, ScrollToEnd, ScrollToHome

UI에서 실행되는 일반적인 명령들을 정의하고 있다.

MediaCommands Defines properties that

BoostBase, ChannelUp, ChannelDown, FastForward, NextTrack, Play, Rewind, Select, Stop

미디어 영역에서 사용되는 명령들을 정의하고 있다.

NavigationCommands 

BrowseBack, BrowseForward, Favorites, LastPage, NextPage, Zoom

WPF의 네비게이션 모델에서 사용되는 명령들을 정의하고 있다.

EditingCommands Defines numerous

AlignCenter, CorrectSpellingError, DecreaseFontSize, EnterLineBreak, EnterParagraphBreak, MoveDownByLine,

WPF의 문서 API를 이용한 프로그래밍 모델에서 사용되는 명령들을 정의하고 있다

[표29-4] WPF에서 지원하는 컨트롤 명령 객체


Command 속성에 명령 지정하기

만약 이러한 명령들을 Command 속성을 지원하고 있는 UI 개체에 적용하고 싶다면 간단하게 지정할 수 있다. 그럼 이 명령을 지정해보기 위해서 Edit 라는 이름의 새로운 명령을 추가하고 복사, 붙이기, 자르기와 같은 텍스트 편집을 지원하는 서브 메뉴를 추가해 보도록 하자.

<Menu DockPanel.Dock ="Top"
HorizontalAlignment="Left" Background="White" BorderBrush ="Black">
  <MenuItem Header="_File" Click ="FileExit_Click" >
    <Separator/>
    <MenuItem Header ="_Exit" MouseEnter ="MouseEnterExitArea"
    MouseLeave ="MouseLeaveArea" Click ="FileExit_Click"/>
  </MenuItem>
  <!--새로운 항목과 명령의 추가 -->
  <MenuItem Header="_Edit">
    <MenuItem Command ="ApplicationCommands.Copy"/>
    <MenuItem Command ="ApplicationCommands.Cut"/>
    <MenuItem Command ="ApplicationCommands.Paste"/>
  </MenuItem>
  <MenuItem Header="_Tools">
    <MenuItem Header ="_Spelling Hints" MouseEnter ="MouseEnterToolsHintsArea"
    MouseLeave ="MouseLeaveArea" Click ="ToolsSpellingHints_Click"/>
  </MenuItem>
</Menu>



자식 항목에 각각의 Command를 설정해 주었다. 이렇게 함으로써 각각의 자식항목들의 이름이 자동으로 지정되고 단축키 또한 자동으로 설정되기 때문에 우리는 이름이나 단축키를 설정하는 코드를 작성하지 않아도 된다는 것을 알아두자. 그렇기 때문에 이렇게 XAML을 추가하고 실행해보면 다음 [그림29-28]처럼 새로운 메뉴들이 보여지는 것을 볼 수 있을 것이다. 
 

[그림29-28] 명령을 이용하면 쉽게 기능을 구현할 수 있다.


임의적인 명령의 설정

만약 Command 속성을 지원하지 않는 UI에 명령을 지정하고 싶다면 코드를 이용해서 명령을 지정해 줄 수 있다. 그 코드가 그렇게 복잡한 것은 아니지만 XAML 보다는 약간의 로직이 더 들어간다. 예를 들어 만약 윈도우 전체에서 F1 키를 눌렀을 때 도움말 창이 실행되게 하려면 어떻게 할 것인지 구현해 보도록 하겠다.

먼저 매인 윈도우에 SetF1CommandBinding() 라는 새로운 메서드를 추가 해보자. 그리고 이 메서드는 InitializeComponent() 이후에 호출할 것이다. 이 메서드에서는 CommandBinding이라는 새로운 객체를 생성한다. 이 때 파라메터로 F1키가 눌렸을 때 자동으로 동작하기 위해서 ApplicationCommands.Help를 설정하도록 하겠다.

private void SetF1CommandBinding()
{
    CommandBinding helpBinding = new CommandBinding(ApplicationCommands.Help);
    helpBinding.CanExecute += CanHelpExecute;
    helpBinding.Executed += HelpExecuted;
    CommandBindings.Add(helpBinding);
}



그리고 CommandBinding 객체에서는 현재 명령의 실행이 가능한지 알려주기 위한 CanExecute 이벤트를 작성해 주고 또한 명령을 실행하기 위한 Executed 이벤트 또한 구현해 주어야 한다. 그럼 이 이벤트들을 다음과 같이 구현해 보도록 하자.

private void CanHelpExecute(object sender, CanExecuteRoutedEventArgs e)
{
    // 여기에서 CanExecute를 fast로 지정하면 명령이 실행되는 것을 막을 수 있다. 
    e.CanExecute = true;
}
private void HelpExecuted(object sender, ExecutedRoutedEventArgs e)
{
    MessageBox.Show("Dude, it is not that difficult. Just type something!",
    "Help!");
}



여기서 우리는 언제나 F1키를 누르면 CanHelpExecute() 메서드가 실행될 수 있게 간단하게 구현하였다. 하지만 만약에 도움말을 보여줄 수 있는 상황이 아니라면 우리는 여기서 false를 반환해서 창을 띄워주지 않게 구현해 주어야 한다. 여기서 HelpExecute()가 실행되면 메시지박스를 보여주게 된다. 이렇게 코드를 작성하고 실행해보면 다음 [그림29-29]와 같은 화면을 볼 수 있을 것이다. 
 

[그림29-29] 도움말 띄우기


WPF 데이터 바인딩의 이해

컨트롤들은 종종 다양한 데이터들을 바인딩 시켜 보여주어야 할 경우가 많이 있다. 간단하게 데이터 바인딩의 정의를 내려 보자면 데이터 바인딩은 컨트롤 속성에 데이터 값을 넣는 것을 의미한다. 그리고 그 값은 애플리케이션의 동작에 의해서 변경될 수 있다. 즉, UI 개체들은 코드에서 지정한 변수의 값을 표현할 수 있다는 것이다. 예를 들어 다음과 같은 작업이 가능하다.

- Boolean 속성을 지정해 주어서 CheckBox 컨트롤의 체크 설정하기
- 관련된 DB 테이블의 데이터를 TextBox 컨트롤에 보여주기
- 특정 폴더의 파일 수를 Integer값으로 Label에 보여주기



WPF에서 지원해주고 있는 데이터 바인딩 엔진을 이용하기 위해서는 소스(Source)와 타겟이 있어야 한다. 데이터 바인딩에서 사용되는 소스는 데이터를 의미한다. (Boolean 속성, 관련된 데이터) 그리고 타켓은 데이터를 보여주기 위해서 사용되는 UI 컨트롤의 속성이 되겠다. 

데이터 바인딩의 타겟 속성은 반드시 UI 컨트롤의 의존성 속성이 되어야 한다.



WPF의 데이터 바인딩의 구조는 굉장히 자유롭다. 만약 개발자가 특정 컨트롤에 데이터 바인딩을 적용하고 싶다면 일반적으로 소스와 타켓의 연결을 위해서 특정 코드를 추가할 것이고 또한 다양한 이벤트들을 지정해 줄 것이다. 예를 들어 만약 Label 컨트롤을 이용해서 어떤 정보를 보여주는 창이 있다고 가정하고 그 창은 ScrollBar를 이용해서 스크롤을 지원하고 있다고 가정해보자. 이 때 아마 ScrollBar의 ValueChange 이벤트를 구현할 것이고 그 이벤트에서는 Label 컨트롤의 값을 수정해 줄 것이다. 

하지만 WPF의 데이터 바인딩은 다양한 이벤트나 코드의 작성 없이도 XAML에서 직접 소스와 타겟을 연결 시킬 수 있다. 뿐만 아니라 어떤 데이터 바인딩 로직을 사용한다 하더라도 값이 변경 되었을 때의 소스와 타겟의 동기를 확실하게 보장받을 수 있다.


데이터 바인딩

그렇다면 먼저 WPF에서 제공하고 있는 데이터 바인딩의 기능을 살펴보기 위해서 SimpleDataBinding라는 이름의 WPF 응용 프로그램 프로젝트를 하나 생성해보자. 그리고 다음과 같은 마크업 언어를 작성해 보도록 하자.


<Window x:Class="SimpleDataBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Simple Data Binding" Height="152" Width="300"
WindowStartupLocation="CenterScreen">
  <StackPanel Width="250">
    <Label Content="Move the scroll bar to see the current value"/>
    <!-- 스크롤바의 값은 데이터 바인딩의 소스(Source)이다. -->
    <ScrollBar Orientation="Horizontal" Height="30" Name="mySB"
    Maximum = "100" LargeChange="1" SmallChange="1"/>
    <!-- Label의 Content 값은 데이터 바인딩의 타겟이다 -->
    <Label Height="30" BorderBrush="Blue" BorderThickness="2"
    Content = "{Binding ElementName=mySB, Path=Value}"
/>
  </StackPanel>
</Window>


mySB라는 이름을 지정해준 <ScrollBar>컨트롤은 0과 100 사이의 값을 설정해줄 수 있다. 오른쪽 왼쪽 버튼을 이용해서 그 값을 수정하게 되면 Label은 자동으로 그 값을 보여주게 될 것이다. 이러한 설정을 위해서 Label의 Content 속성에 Binding이라는 마크업 확장식을 이용하였다. 그리고 ElementName의 값에는 데이터 바인딩의 소스를 지정해주었고 Path에는 그 소스에서 값이 되는 속성을 지정해 준 것이다.


여기서 “Source” 와 “Destination(타겟)” 이라는 지관적인 이름 대신 ElementName과 Path가 조금 이상해 보일 수 있다. 뒷부분에서 살펴볼 내용이지만 XML 문서들이 데이터 바인딩의 소스가 될 수 있다. 이 경우에는 ElementName과 Path라는 이름이 더 적절하다.


이러한 포맷 대신에 DataContext라는 속성을 이용해서 {Binding}을 지정하는 것이 가능하다. 다음 코드를 살펴보자.

<!-- DataContext의 이용 -->
<Label Height="30" BorderBrush="Blue" BorderThickness="2"
DataContext = "{Binding ElementName=mySB}"
Content = "{Binding Path=Value}"
/>



어떻게 셋팅을 한다 하더라도 애플리케이션을 실행해보면 다음 [그림29-30]처럼 동작되는 것을 볼 수 있을 것이다.
 

[그림29-30]ScrollBar 값을 Label로 바인딩


DataContext 속성

앞의 예제에서는 소스와 타켓 두 가지를 모두 지정한 데이터 바인딩의 예제를 살펴보았다. 그렇다면 DataContext는 언제 사용하는 것이지 아마 궁금할 것이다. 이 속성은 의존성 속성이기 때문에 매우 유용하다. 즉, 이 속성은 자식 개체로 상속시킬 수 있는 것이다. 그럼 다음과 같은 XAML을 작성해 보도록 하자.


<!-- StackPanel에 DataContext속성을 설정하였다 -->
<StackPanel Width="250" DataContext = "{Binding ElementName=mySB}">
  <Label Content="Move the scroll bar to see the current value"/>
  <ScrollBar Orientation="Horizontal" Height="30" Name="mySB"
  Maximum = "100" LargeChange="1" SmallChange="1"/>
  <!-- Now both UI elements use the scrollbar's value in unique ways. -->
  <Label Height="30" BorderBrush="Blue" BorderThickness="2"
  Content = "{Binding Path=Value}"/>
  <Button Content="Click" Height="200"
  FontSize = "{Binding Path=Value}"/>
</StackPanel>



여기서 DataContext 속성은 <StackPanel>에서 직접적으로 선언하였다. 때문에 스크롤을 움직이면 현재 Label의 값 뿐만 아니라 Button의 폰트 크기 또한 같은 값으로 설정되는 것을 볼 수 있다. 이 프로그램을 실행하면 다음 [그림29-31]과 같은 화면을 볼 수 있을 것이다.
 

[그림29-31] ScrollBar 값을 Label과 Button에 데이터 바인딩


Mode 속성

데이터 바인딩에서 Mode 속성에 데이터 바인딩 동작 모드를 설정할 수 있다. 기본적으로 Mode 속성은 OneWay로 설정되어 있고 이것은 타겟의 값이 변경된다 값을 제공한 소스에서는 어떤 영향도 주지 않게 된다. 예를 들어 앞에서 살펴본 예제에서 라벨의 Content 속성을 변경한다 하더라도 ScrollBar의 값은 변하지 않는다는 것이다.

만약 소스와 타겟을 동기화 시키고 있다면 Mode 속성을 TwoWay로 설정하면 된다. 이렇게 설정하면 라벨의 값(Content)이 수정되면 스크롤의 위치도 같이 움직일 것이다. 물론 Label에서는 Content의 값을 사용자가 바꿀 수 없기 때문에 프로그램에서 바꾸지 않는 한 직접 확인하기 힘들 것이다. 

TwoWay 모드를 사용해 보기 위해서 Label을 TextBox로 대체 시키도록 하자. 이렇게 설정한 후에 사용자가 텍스트를 입력하고 탭 키를 눌러보면 그 값이 자동으로 업데이트 되는 것을 볼 수 있을 것이다. 


<TextBox Height="30" BorderBrush="Blue"
BorderThickness="2" Text = "{Binding Path=Value}"/>



Mode 속성은 한번만 설정할 수 있다. 한번 설정되면 나중에 변경하는 것이 불가능 하다.



IValueConverter를 이용하여 데이터 변환

ScrollBar 컨트롤은 숫자 값이 아닌 double 형의 값을 이용한다. 그렇기 때문에 스크롤을 움직여서 설정된 값을 TextBox 컨트롤에 조회해보면 61.0576923076923와 같은 상당히 긴 소수점들이 출력되는 것을 볼 수 있을 것이다. 하지만 이 값은 사용자들에게 직관적이지 못하다. 대부분의 사용자들은 61, 62, 63과 같은 숫자를 보기 원할 것이다.

데이터 바인딩에서 이러한 데이터 포맷을 변경해주고 싶을 것이다. 이 경우 System.Windows.Data 네임스페이스에서 지원하는 IValueConverter 인터페이스를 상속받은 커스텀 클래스를 만들어야 한다. 이 인터페이스는 데이터 변환을 위한 2개의 멤버를 가지고 있다. 그렇기 때문에 이 클래스를 정의해서 데이터를 변환시켜 줄 수 있다.

그럼 클래스를 생성하여 TextBox 컨트롤에 숫자만 보여줄 수 있게 설정해 보도록 하자. (먼저 System.Windows.Data를 선언해야 한다.)

class MyDoubleConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter,
    System.Globalization.CultureInfo culture)
    {
        // double을 int로 변경
        double v = (double)value;
        return (int)v;
    }
    public object ConvertBack(object value, Type targetType, object parameter,
    System.Globalization.CultureInfo culture)
    {
        // 값의 반환
        // 이 값은 TwoWay 바인딩에 이용된다.
        // 사용자가 TextBox에 탭을 눌렀을 때 발생하게 된다. (이번 예제에서)
        return value;
    }
}


Convert() 메서드는 소스(ScrollBar)의 값이 변경되어 타겟(TextBox의 Text 속성)으로 값을 전달할 때 호출된다. 우리는 object 타입의 값을 파라메터로 받을 수 있고 이 값은 double로 선언되어 있을 것이다. 이 값을 int 형으로 간단히 변경하여 넘겨주게 된다.

ConvertBack() 메서드는 TwoWay 바인딩에서만 사용 가능한 메서드로 타겟에서 소스로 값을 전달할 때 사용된다. 여기서는 간단히 바로 value값을 반환하였다. 이렇게 설정하게 되면 TextBox에는 소수점이 아닌 숫자가 전달하게 될 것이다. 먼저 Convert() 메서드가 호출될 것이고 그 다음에 ConvertBack() 메서드가 호출될 것이다. 만약 ConvertBack() 메서드가 null을 반환한다면 양방향으로 동기화가 이루어지지 않을 것이다.

이 클래스를 사용하도록 XAML에 적용해 보도록 하겠다.

<Window x:Class="SimpleDataBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  <!-- CLR 네임스페이스의 정의 -->
  xmlns:myConverters ="clr-namespace:SimpleDataBinding"
  Title="Simple Data Binding" Height="334" Width="288"
  WindowStartupLocation="CenterScreen">
  <!-- Resource dictionaries allow us to define objects that can
be obtained by their key. More details in Chapter 30. -->
  <Window.Resources>
    <myConverters:MyDoubleConverter x:Key="DoubleConverter"/>
  </Window.Resources>
  <!-- ScrollBar를 DataContext로 설정 -->
  <StackPanel Width="250" DataContext = "{Binding ElementName=mySB}">
    <Label Content="Move the scroll bar to see the current value"/>
    <ScrollBar Orientation="Horizontal" Height="30" Name="mySB"
    Maximum = "100" LargeChange="1" SmallChange="1"/>
    <!-- {Binding} 확상식을 이용해서 클래스 설정 -->
    <TextBox Height="30" BorderBrush="Blue" BorderThickness="2" Name="txtThumbValue"
    Text = "{Binding Path=Value, Converter={StaticResource DoubleConverter}}"/>
    <Button Content="Click" Height="200"
    FontSize = "{Binding Path=Value}"/>
  </StackPanel>
</Window>


ML 네임스페이스는 프로젝트의 최상위 네임스페이스와 연결시켜 주어야 한다. 그리고 Window의 리소스에 MyDoubleConverterfmf 추가하였다. 이 때 Key값을 DoubleConverter로 지정해 주었고 XAML에서는 DoubleConverter라는 이름으로 선언해 줄 것이다. TextBox의 Convert 속성에서는 StaticResource라는 마크업 확장식을 이용해서 MyDoubleConverter의 Key 값을 참조해 주었기 때문에 Text 속성의 값은 숫자로 변경되어 지정될 것이다. 리소스에 대한 내용은 30장에서 더 자세히 살펴보도록 하겠다. 


다른 데이터 타입의 변환

IValueConverter 인터페이스를 구현하면 그 데이터들이 어떤 연관이 없다 하더라도 데이터 타입을 변환하는 것이 가능하다. 예를 들자면 ScrollBar의 반환 값을 어떠한 값으로도 변환이 가능하다라는 것이다. 그럼 ColorConverter를 이용해서 double 값을 SolidColorBrush로 변환해주는 클래스를 살펴 보도록 하자.

class MyColorConverter : IValueConverter

{

    public object Convert(object value, Type targetType, object parameter,

    System.Globalization.CultureInfo culture)

    {

        // ¥UucN Green Æ¨£uⓒø¢´

        double d = (double)value;

        byte v = (byte)d;

        Color color = new Color();

        color.A = 255;

        color.G = (byte)(155 + v);

        return new SolidColorBrush(color);

    }

    public object ConvertBack(object value, Type targetType, object parameter,

    System.Globalization.CultureInfo culture)

    {

        return value;

    }

}



이렇게 클래스를 작성했다면 리소스에 다음과 같이 추가할 수 있다. 

<Window.Resources>

  <myConverters:MyDoubleConverter x:Key="DoubleConverter"/>

  <myConverters:MyColorConverter x:Key="ColorConverter"/>

</Window.Resources>


그럼 이 리소스 값을 Button의 Background 속성에 적용해 보도록 하자.

<Button Content="Click" Height="200"

FontSize = "{Binding Path=Value}"

Background"{Binding Path=Value, Converter={StaticResource ColorConverter}}"/>


이렇게 수정한 다음에 프로그램을 실행해보면 스크롤의 위치에 따라 버튼의 색이 변하는 것을 볼 수 있을 것이다. 지금까지 WPF의 데이터 바인딩에 대해서 살펴 보았다. 다음에는 커스텀 객체와 UI 개체와 어떻게 연동시킬 것인지 살펴보도록 하자.


커스텀 객체의 바인딩

이전에는 커스텀 객체들을 UI 개체에 어떻게 바인딩 시키는지 살펴보도록 하겠다. CarViewerApp 라는 WPF 애플리케이션 프로젝트를 생성하고 Window1 클래스의 이름을 MainWindow로 변경해 보도록 하자. 그리고 MainWindow에 Loaded 이벤트를 지정해주고 2개의 Row와 Column을 가지는 그리드를 추가해 보도록 하자.

<Window x:Class="CarViewerApp.MainWindow"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

Title="Car Viewer Application" Height="294" Width="502"

ResizeMode="NoResize" WindowStartupLocation="CenterScreen"

Loaded="Window_Loaded"

> 

  <Grid>

    <Grid.ColumnDefinitions>

      <ColumnDefinition Width="200"/>

      <ColumnDefinition Width="*"/>

    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>

      <RowDefinition Height="Auto"/>

      <RowDefinition Height="*"/>

    </Grid.RowDefinitions>

  </Grid>

</Window>



 
그리고 <Grid>의 첫 번째 행은 메뉴 컨트롤을 추가해 보겠다. 이 때 File이라는 상위 메뉴를 추가하고 New Car와 Exit라는 자식 메뉴를 추가할 것이다. 그리고 서브 메뉴들에는 각각의 Click 이벤트를 추가할 것고 Exit 메뉴는 InputGestureText속성에 “Alt+F4”를 할당해 줄 것이다. 마지막으로 Grid.ColumnSpan의 값을 2로 설정해줄 것이다.

<!-- 메뉴 바 -->
<DockPanel
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="0">
  <Menu DockPanel.Dock ="Top" HorizontalAlignment="Left" Background="White">
    <MenuItem Header="File">
      <MenuItem Header="New Car" Click="AddNewCarWizard"/>
      <Separator />
      <MenuItem Header="Exit" InputGestureText="Alt-F4"
      Click="ExitApplication"/>
    </MenuItem>
  </Menu>
</DockPanel>


그리고 나머지 <Grid>의 왼쪽 영역은 <DockPanel>을 추가한 후에 ListBox를 추가하고 오른쪽에는 하나의 TextBlock을 추가하도록 하자. ListBox 컨트롤은 커스텀 객체를 이용해서 데이터 바인딩을 하게 되는 타겟이 될 것이다. 그렇기 때문에 ItemsSource 속성을 {Binding} 마크업 확장식으로 설정하도록 하자. 사용자가 ListBox를 선택하면 SelectionChanged 이벤트를 이용해서 TextBlock의 내용을 수정해 주도록 할 것이다. 그럼 다음 XAML을 추가해 주도록 하자.


<!-- Grid의 왼쪽 영역 -->
<ListBox Grid.Column="0"
Grid.Row="2" Name="allCars" SelectionChanged="ListItemSelected"
Background="LightBlue" ItemsSource="{Binding}">
</ListBox>
<!-- 그리드의 오른쪽 영역 -->
<TextBlock Name="txtCarStats" Background="LightYellow"
Grid.Column="1" Grid.Row="2"/>



이렇게 윈도우 UI를 작성했다면 다음 [그림29-32]와 같은 모습을 볼 수 있을 것이다. 데이터 바인딩을 구현하기 전에 먼저 Exit 메뉴를 클릭했을 때의 코드를 먼저 추가하도록 하자.

private void ExitApplication(object sender, RoutedEventArgs e)

{

    Application.Current.Shutdown();

}




[그림29-32] 매인 윈도우의 UI


ObservableCollection<T> 다루기

닷넷 3.0에서는 System.Collections.ObjectModel 네임스페이스에서 정의하고 있는 ObservableCollection<T>라는 컬렉션이 새롭게 추가되었다. 이 컬렉션의 장점은 UI가 업데이트 되었을 경우 그 값이 변경되었다는 알람을 받을 수 있다는 것이다. 예를 들어 TwoWay 바인딩처럼 말이다. 그럼 새로운 CarList 라는 C#파일을 새로 추가해보도록 하자. 이 클래스에서는 ObservableCollection<T>를 확장할 것이고 T는 Car로 지정할 것이다. Car 클래스는 생성자를 이용해서 데이터들을 저장할 것이고 또 ToString()과 같은 메서드를 추가로 구현해 보도록 하겠다.


using System;

using System.Collections.ObjectModel;

namespace CarViewerApp

{

    public class CarList : ObservableCollection<Car>

    {

        public CarList()

        {

            // 몇가지 리스트 추가

            Add(new Car(40, "BMW""Black""Sidd"));

            Add(new Car(55, "VW""Black""Mary"));

            Add(new Car(100, "Ford""Tan""Mel"));

            Add(new Car(0, "Yugo""Green""Clunker"));

        }

    }

    public class Car

    {

        public int Speed { getset; }

        public string Make { getset; }

        public string Color { getset; }

        public string PetName { getset; }

 

        public Car(int speed, string make, string color, string name)

        {

            Speed = speed; Make = make; Color = color; PetName = name;

        }

        public Car() { }

        public override string ToString()

        {

            return string.Format("{0} the {1} {2} is going {3} MPH",

            PetName, Color, Make, Speed);

        }

    }

}



이제 MainWindow 클래스를 열어서 myCars라는 CarList변수를 정의하도록 하자. 그리고 Loaded 이벤트에서 allCars 변수(ListBox)의 DataContext 속성에 MyCar 객체를 할당해주는 코드를 작성해 보도록 하자. (XAML에서는 이 값을 설정하지 않았다.)


private void Window_Loaded(object sender, RoutedEventArgs e)
{
    // DataContext의 설정
    allCars.DataContext = myCars;
}



이렇게 작성한 후에 프로그램을 실행시켜 보면 다음 [그림29-33]처럼 ListBox안에 각각의 자동차들이 ToString()되어 출력되는 것을 볼 수 있을 것이다.
 

[그림29-33] 최초의 데이터 바인딩


커스텀 데이터 템플릿 만들기

ListBox를 보면 CarList안에 정의되었던 각각의 항목들이 보여지는 것을 볼 수 있을 것이다. 하지만 우리는 특별히 바인딩 Path를 지정하지 않았기 때문에 각각의 서브 객체들의 ToString()을 호출하여 보여지게 된 것이다. 우리는 앞에서 바인딩 Path를 어떻게 지정해 줄 수 있는지 살펴보았다. 이번에는 커스텀 데이터 템플릿을 선언해 볼 것이다. 간단하게 말해서 데이터 템플릿은 현재의 데이터를 어떠한 형식으로 보여줄 것인지를 지정하는 폼이라고 보면 된다. 우리의 예제에서는 ListBox에서 단순한 문자열이 호출되었지만 이번에는 각각의 아이템들이 <StackPanel>을 가지고 Ellipse와 TextBlock을 가질 수 있게 설정해 볼 것이다. 그리고 TextBlock에는 CArList의 PetName을 지정해 줄 것이다. 다음과 같이 코드를 수정해 보도록 하자.

<ListBox Grid.Column="0"

Grid.Row="2" Name="allCars" SelectionChanged="ListItemSelected"

Background="LightBlue" ItemsSource="{Binding}">

  <ListBox.ItemTemplate>

    <DataTemplate>

      <StackPanel Orientation="Horizontal">

        <Ellipse Height="10" Width="10" Fill="Blue"/>

        <TextBlock FontStyle="Italic" FontSize="14" Text="{Binding Path=PetName}"/>

      </StackPanel>

    </DataTemplate>

  </ListBox.ItemTemplate>

</ListBox>



여기서 <ListBox.ItemTemplate>을 이용해서 <DataTemplate>를 선언해 주었다. 결과를 보기 전에 SelectionChanged 이벤트를 구현해 보도록 하자. 이 이벤트에서는 현재 선택한 항목을 Tostring()을 호출해서 그 값을 TextBlock에 출력해 보도록 할 것이다.

private void ListItemSelected(object sender, SelectionChangedEventArgs e)
{
    // 현재 선택된 ObservableCollection의 항목 가져 온 다음에 ToString() 호출
    txtCarStats.Text = myCars[allCars.SelectedIndex].ToString();
}


이렇게 업데이트 한 후에 실행 해보면 다음 [그림29-34]와 같은 화면이 출력되는 것을 볼 수 있을 것이다. 
 

[그림29-34] 커스텀 템플릿의 데이터 바인딩


UI 개체에 XML문서의 바인딩

이번에는 커스텀 대화상자를 만들어 보도록 하겠다. 이 대화상자는 XML 파일을 열어 ListView에 바인딩 키킬 것이다. 먼저 Inventory.xml을 준비해야 하는데 이 파일은 24장의 NavigationWithLinqToXml 프로젝트에서 사용했었다. 그럼 이 파일을 복사해서 [기존항목추가]를 이용해서 프로젝트에 추가 시키도록 하자. 그리고 솔루션 탐색기에서 이 아이템을 선택한 후에 속성 창을 열어 보면 “출력 디렉토리 복사”라는 메뉴가 있을 것이다. 여기서 항상 복사를 선택하자. 그럼 컴파일을 하게 되면 이 파일은 \bin\Debug에 자동으로 복사될 것이다.


커스텀 대화상자 만들기

AddNewCarDialog라는 새로운 Window 컨트롤을 추가해보도록 하자. 이 새 창에서는 Inventory.xml 파일을 ListView 컨트롤로 바인딩 시킬 것이다. 먼저 새창의 UI를 설정해 보도록 하자. 먼저 다음과 같은 XAML을 추가한다.





<Window>에 ResizeMode 속성을 NoResize로 설정하였다. 때문에 대화상자의 크기는 사용자가 임의로 크기를 조절할 수 없다. 

<Grid>는 2개의 행을 선언하였고 그리드 리소스로 XmlDataProvider라는 리소스를 추가하였다. 이 리소스는 외부의 XML 파일과 연결시켜 주는 역할을 하게 된다. 현재 프로젝트에서는 Inventory.xml 파일을 추가하였고 실행될 애플리케이션과 같은 경로에서 위치하게 설정했기 때문에 추가적인 경로를 지정하지 않고 바로 파일이름만 넣어주었다.

그 다음은 거의 ListView에 대한 내용이다. 먼저 ItemsSource 애트리뷰트에 CarsXmlDoc 리소스를 할당해 주었다. 그리고 XPath를 이용해서 XML 경로를 할당해 주었는데 이 속성은 XML 문서를 어디서부터 탐색할 것인지 초기화 위치를 설정해 주면 된다. 여기서는 <Inventory>루트에 <Car>라는 경로로 초기화 해주었다.

ListVeiw에 출력될 아이템들은 <ListView.View>를 이용하하여 <GridView>를 정의하였다. 그리고 4개의 <GridViewColumns>를 구성하였다. 각각의 <GridViewColumns>항목에는 Header값을 지정해 주었고 DisplayMemberBinding를 통하여 데이터 바인딩 값을 지정해 주었다. <ListView>에서는 이미 초기 XML 문서 경로를 <Inventory>의 <Car>로 지정해 주었기 때문에 각각의 컬럼에서는 그 다음의 항목들을 바로 지정해 주었다. 

첫 번째 <GridViewColumn>에서는 XPath 문법을 이용해서 @ID를 지정해주었다. 이렇게 @를 붙여 지정하게 되면 <Car>엘리먼트의 에트리뷰트 값을 가져오게 된다. 그리고 다음 컬럼에서는 Car 하위의 엘리먼트 요소들을 정의하였다.

마지막으로 <Grid>의 아래 행에는 2개의 Button 컨트롤을 가지고 있는 <WrapPanel>을 추가하였다. 이 버튼은 IsDefault 속성과 IsCancel 속성들을 기본적으로 사용하게 설정하였다. 이렇게 구현하게 되면 Click 이벤트가 발생하면 기본적으로 Enter 키가 눌려지거나 ESC 키가 눌려지게 된다.

마지막으로 이 버튼들에는 TabIndex 값과 Margin 값을 지정해 주었다는 것을 알아두자. Margin의 경우 <WrapPanel>에 정의되는 여백이다. 
DialogResult값의 할당

새로운 대화상자를 실행해 보기 전에 OK 버튼을 눌렀을 때의 Click 이벤트를 작성해야 한다. 27장에서 살펴본 윈도우 폼과 비슷하게 WPF 대화상자는 DialogResult를 설정해 줄 수 있다. 하지만 윈도우 폼의 DialogResult 속성과는 다른 점은 WPF에서 이 속성은 nullable Boolean 값으로 선언되었다는 것이다. 그렇기 때문에 OK 버튼에서는 DialogResult 속성을 true로 할당해 주었다.


<Window x:Class="CarViewerApp.AddNewCarDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="AddNewCarDialog" Height="234" Width="529"
ResizeMode="NoResize" WindowStartupLocation="CenterScreen" >
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="144" />
      <RowDefinition Height="51" />
    </Grid.RowDefinitions>
    <!-- XmlDataProvider 이용-->
    <Grid.Resources>
      <XmlDataProvider x:Key="CarsXmlDoc"
      Source="Inventory.xml"/>
    </Grid.Resources>
    <!-- Xpath에 애트리뷰트/엘리먼트 형식으로 지정해준다. -->
    <ListView Name="lstCars" Grid.Row="0"ItemsSource=
"{Binding Source={StaticResource CarsXmlDoc}, XPath=/Inventory/Car}"
>
      <ListView.View>
        <GridView>
          <GridViewColumn Width="100" Header="ID"
          DisplayMemberBinding="{Binding XPath=@ID}"/>
          <GridViewColumn Width="100" Header="Make"
          DisplayMemberBinding="{Binding XPath=Make}"/>
          <GridViewColumn Width="100" Header="Color"
          DisplayMemberBinding="{Binding XPath=Color}"/>
          <GridViewColumn Width="150" Header="Pet Name"
          DisplayMemberBinding="{Binding XPath=PetName}"/>
        </GridView>
      </ListView.View>
    </ListView>
    <WrapPanel Grid.Row="1">
      <Label Content="Select a Row to Add to your car collection" Margin="10" />
      <Button Name="btnOK" Content="OK" Width="80" Height="25"
      Margin="10" IsDefault="True" TabIndex="1" Click="btnOK_Click"/>
      <Button Name="btnCancel" Content="Cancel" Width="80" Height="25"
      Margin="10" IsCancel="True" TabIndex="2"/>
    </WrapPanel>
  </Grid>
</Window>



private void btnOK_Click(object sender, RoutedEventArgs e)
{
    DialogResult = true;
}


DialogResult의 기본값을 false이기 때문에 만약 사용자가 Cancel 버튼을 눌렀을 때의 이벤트를 작성하지 않아도 된다.


선택 값 전달하기

마지막으로 SelectedCar라는 읽기전용의 속성을 추가해 보도록 하자. 이 속성은 부모 창에 현재 선택한 Car 객체를 반환해주는 역할을 하게 될 것이다.

public Car SelectedCar
{
    get
    {
        // 선택 항목을 XmlElement로 변환
        System.Xml.XmlElement carRow =
        (System.Xml.XmlElement)lstCars.SelectedItem;
        // 사용자가 선택하지 않았을 경우
        if (carRow == null)
        {
            return null;
        }
        else
        {
            // 랜덤 속도 설정
            Random r = new Random();
            int speed = r.Next(100);
            // XML값을 읽어와 Car 객체를 생성하여 반환한다.
            return new Car(speed, carRow["Make"].InnerText,
            carRow["Color"].InnerText, carRow["PetName"].InnerText);
        }
    }
}



우리는 SelectedItem 값을 XmlElement 타입으로 변환하여 전달하였다. ListView는 Inventory.xml 파일을 이용해서 데이터 바인딩 했기 때문에 이러한 변환이 가능한 것이다. 그리고 이 객체를 가져와서 Make, Color, PetName 값을 InnerText를 이용해서 문자열을 추출하였다. 


만약 System.Xml 네임스페이스를 다루어 본적이 없다면 InnerText 속성을 이해하기 힘들 것이다. 이 값은 XML에서 특정 열고 닫은 엘리먼트 사이에 지정된 값이라고 보면 된다. 예를 들어 <Make>Ford</Make> 이러한 값이 있다면 Ford 값이 InnerText 값이 되는 것이다.



커스텀 대화상자 보여주기

이렇게 해서 커스텀 대화상자를 모두 구현해 보았다. 그렇다면 이 창을 띄우기 위해서 [File]의 [Add New Car] 메뉴에 Click 이벤트를 지정하고 다음과 같이 구현해보자.

private void AddNewCarWizard(object sender, RoutedEventArgs e)
{
    AddNewCarDialog dlg = new AddNewCarDialog();
    if (true == dlg.ShowDialog())
    {
        if (dlg.SelectedCar != null)
        {
            myCars.Add(dlg.SelectedCar);
        }
    }
}



윈도우 폼과 같이 WPF 대화상자는 모달창이나 아니면 일반창으로 띄울 수 있다. 만약 ShowDialog() 값이 true라면 우리는 새로운 Car 객체를 요청할 것이고 ObservableCollection<T> 객체에 그 값을 추가하게 될 것이다. 이 컬렉션은 컬렉션이 수정되었을 때 자동으로 알람을 보내주기 때문에 ListBox는 자동적으로 새로운 항목이 추가될 것이다. 다음 [그림29-35]는 지금까지 구현한 대화상자의 UI를 보여주고 있다.
 

[그림29-35] XML 문서를 바인딩 한 대화상자


지금까지 WPF 데이터 바인딩 엔진에 대해서 살펴 보았고 컨트롤에서 지원되고 있는 UI API들에 대해서 살펴보았다. 다음 장에서는 그래픽 렌더링, 리소스 관리, 커스텀 테마의 선언과 같은 WPF 기술들에 대해서 계속 살펴볼 것이다.

지금까지 WPF 컨트롤, 의존속성의 이해, 이벤트 라우팅과 같은 여러 가지 내용들을 살펴 보았다. 이러한 WPF 기술들은 데이터 바인딩, 애니메이션등과 같은 프로그래밍 모델에서 많이 사용되기 때문에 굉장히 중요하다. 이번 장을 통해서 다양한 패널들을 활용하는 방법과 여러 컨트롤들을 설정하는 방법들을 살펴볼 수 있었을 것이다.

또한 WPF 명령에 대해서 살펴보았다. 이 컨트롤 명령은 별도의 이벤트를 만드는 수고를 덜어준다는 것을 알아두자. 그리고 WPF의 데이터 바인딩에 대해서 소개했고 속성 값을 이용한 바인딩, 커스텀 객체를 이용한 바인딩 그리고 XML 문서를 이용한 바인딩에 대해서 살펴보았다. 그리고 WPF 대화상자를 어떻게 다루는 살펴보았고 IValueConverter와 ObservableCollection<T>의 역할에 대해서도 살펴보았다. 

이 내용은 C#.NET Platform 원서의 내용을 기반으로 작성되었습니다.