프로그래밍/C#/WPF2015.06.16 17:56

1. 데이터 바인딩 배경

WPF에서 데이터 바인딩이란 XAML로 표현되는 UI 요소와 ViewModel로 표현되는 데이터 사이에 관계를 맺는 기술을 의미한다. 이를 통해 UI 부분과 데이터 부분을 서로 독립적으로 다룰 수 있으며, 이것은 곧 UI 디자이너와 개발자의 역할을 보다 분명하게 정의하고 협업을 효율적으로 할 수 있게 한다.

또한, 굳이 UI 디자이너와 개발자 사이의 협업 관계를 고려하지 않는다 해도 애플리케이션 로직(데이터)과 프레젠테이션(UI) 부분을 분명하게 분리한다는 것은 개발 생산성이나 추후 유지보수성을 고려해보았을 때 매우 바람직한 시도라 할 수 있다. 실제로 이러한 시도의 일환으로 과거부터 존재했던 수 많은 MVC 프레임워크를 일일이 논하지 않아도 많은 개발자들은 이미 애플리케이션 로직과 프레젠테이션 부분을 분리하는데 많은 관심과 노력을 기울이고 있다는 것을 알 수 있다.



2. 데이터 바인딩 구조

WPF 데이터 바인딩을 구성하는 요소는 크게 3가지로 생각해 볼 수 있다. UI 요소를 의미하는 바인딩 대상(보다 정확하게는 UI요소의 DependencyObject 속성), 데이터를 의미하는 바인딩 소스 그리고 이 둘 사이의 관계를 맺어주는 바인딩 개체가 바로 그것이다.

바인딩 개체가 제공하는 바인딩 방식은 4가지가 있다.


첫 번째, OneWay방식은 바인딩 소스에서 바인딩 대상 방향으로만 데이터 바인딩을 제공한다. 예를 들어 TextBox의 Text 속성(바인딩 대상)에 바인딩 된 string 객체(바인딩 소스)가 있다면, string 객체를 수정했을 때 수정 된 값이 TextBox에 반영된다. 그러나 반대로 TextBox의 Text속성이 변경되어도 여기에 바인딩 된 string 객체는 변경되지 않는 않는다.


두 번째, TwoWay는 바인딩 소스와 바인딩 대상 양방향 모두 데이터 바인딩을 제공한다. 즉, OneWay에서 TextBox의 Text 속성을 변경하면 여기에 바인딩 된 string 객체의 값도 함께 변경된다는 것을 의미한다. 대부분의 UI 요소는 기본값으로 OneWay 바인딩을 사용하지만 TextBox의 Text속성, CheckBox의 IsChecked 속성 등은 TwoWay 바인딩을 기본값으로 한다.


세 번째, OneWayToSoruce는 OneWay방식의 반대로 동작한다.

네 번째, 아래 그림에서는 표현되어 있지 않지만 OneTime 방식으로 최초 바인딩 소스 값이 바인딩 대상 속성 값을 초기화 하지만 그 이후는 어떤 변환도 바인딩 대상, 바인딩 소스 모두에 반영되지 않는 방식이다.


위 4가지 데이터 바인딩 방식에서 조금 더 관심을 가져야 하는 부분은 TwoWay 바인딩 방식이다.


TwoWay 바인딩은 바인딩 대상의 속성 값이 변경되면, 해당 변경 내용을 다시 바인딩 소스로 전파하는데 이 과정을 UpdateSourceTrigger라 한다. (OneWayToSource 바인딩도 UpdateSourceTrigger를 정의할 수 있다.)


UpdateSourceTrigger

설명

LostFocus

UI 요소가 포커스를 잃었을 때 바인딩 소스를 업데이트한다.

) TextBox가 포커스를 잃었을 때 TextBoxText 속성 값을 여기에 바인딩 된 string 객체로 전파한다.

PropertyChanged

UI요소의 바인딩 된 속성값이 변경될 때 바인딩 소스를 업데이트한다.

) TextBoxText 속성값이 변경되면 여기에 바인딩 된 string 객체로 전파한다.

Explicit

애플리케이션에서 명시적으로 UpdateSource를 호출할 때

) 사용자가 특정 버튼을 클릭했을 때 UpdateSoruce를 실행해 TextBoxText 속성값을 여기에 바인딩 된 string 객체로 전파한다.



3. 예제

먼저 데이터 바인딩을 사용하지 않을 경우 나타날 수 있는 애플리케이션 코드를 살펴보자.


아래  XAML은 사용자 로그인을 처리하기 위해 아이디와 비밀번호를 입력 받고, 로그인 처리를 실행하기 위한 1개의 버튼을 정의하고 있다. 2개의 TextBox와 1개의 버튼은 x:Name을 통해 고유 식별자를 정의하고 있는데 이것은 XAML의 Code Behind(XAML파일명에 .cs확장자를 더한 클래스 파일이다. 예를 들어 MainWindow.xaml은 MainWindow.xaml.cs라는 Code Behind 파일을 지니게 된다.)에서 해당 UI 요소를 접근하는데 사용된다.

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="300" Width="400">

    <StackPanel>
        <StackPanel.Resources>
            <Style TargetType="StackPanel">
                <Setter Property="Margin" Value="5"/>
            </Style>
            <Style TargetType="Label">
                <Setter Property="Width" Value="80"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
            </Style>
            <Style TargetType="TextBox">
                <Setter Property="Width" Value="120"/>
            </Style>
            <Style TargetType="Button">
                <Setter Property="Width" Value="50"/>
                <Setter Property="Height" Value="25"/>
            </Style>
        </StackPanel.Resources>

        <StackPanel Orientation="Horizontal">
            <Label Content="아이디"/>
            <TextBox x:Name="ID"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <Label Content="비밀번호"/>
            <TextBox x:Name="Passwd"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <Button x:Name="LoginBtn" Content="로그인"/>
        </StackPanel>
    </StackPanel>

</Window>

아래 코드는 위 XAML의 Code Behind 내용 이다. x:Name으로 명명된 식별자를 통해 TextBox의 Text 속성의 값을 읽거나 설정할 수 있다. Button 역시 x:Name으로 명명된 식별자를 통해 Click 이벤트 등을 제어할 수 있다.


로그인 화면 구성이 비교적 간단하고, 입력 값 검증 규칙이 복잡하지 않기 때문에 Code Behind에 UI 요소와 데이터를 처리하는 로직이 뒤섞여 있어도 불편함이 크게 들어나지는 않는다. 그러나 화면을 구성하는 UI 요소의 개수가 증가하고, 입력 값 검증 로직이 복잡해짐에 따라 Code Behind의 복잡도도 크게 증가하게 될 것을 쉽게 예상할 수 있다.

using System.Windows;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        private string LoginID { get; set; }
        private string LoginPasswd { get; set; }

        public MainWindow()
        {
            InitializeComponent();
            this.Loaded += OnLoaded;
            this.LoginBtn.Click += LoginButtonClick;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            Keyboard.Focus(this.ID);
        }

        private void LoginButtonClick(object sender, RoutedEventArgs e)
        {
            LoginID = this.ID.Text;
            LoginPasswd = this.Passwd.Text;
            if (string.IsNullOrEmpty(LoginID))
            {
                MessageBox.Show("아이디를 입력해주세요.");
                Keyboard.Focus(this.ID);
                return;
            }
            if (string.IsNullOrEmpty(LoginPasswd))
            {
                MessageBox.Show("비밀번호를 입력해주세요.");
                Keyboard.Focus(this.Passwd);
                return;
            }
            doLogin();
        }

        private bool doLogin()
        {
            //-- 로그인 처리
            MessageBox.Show(string.Format("아이디={0}, 비밀번호={1}", LoginID, LoginPasswd));
            return true;
        }
    }
}

위 코드를 데이터 바인딩을 적용해 UI 부분과 데이터 부분을 유연하게 분리해 보자.


먼저 변경 된 XAML은 아래와 같다앞서 살펴본 XAML과는 크게 차이가 나지 않지만 TextBoxText 속성에 바인딩 구문이 사용되고 있음을 주목하자.

<Window x:Class="WpfApplication1.LoginWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="LoginWindow" Height="300" Width="400">

    <StackPanel>
        <StackPanel.Resources>
            <Style TargetType="StackPanel">
                <Setter Property="Margin" Value="10"/>
            </Style>
            <Style TargetType="Label">
                <Setter Property="Width" Value="80"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
            </Style>
            <Style TargetType="TextBox">
                <Setter Property="Width" Value="120"/>
            </Style>
            <Style TargetType="Button">
                <Setter Property="Width" Value="50"/>
                <Setter Property="Height" Value="25"/>
            </Style>
        </StackPanel.Resources>

        <StackPanel Orientation="Horizontal">
            <Label Content="아이디"/>
            <TextBox x:Name="ID" Text="{Binding LoginID, UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <Label Content="비밀번호"/>
            <TextBox x:Name="Passwd" Text="{Binding LoginPasswd, UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <Button x:Name="LoginBtn" Content="로그인"/>
        </StackPanel>
    </StackPanel>

</Window>

Code Behinde의 내용은 다음과 같다. this.DataContext에 LoginViewModel을 설정하고 있는 부분이 앞서 살펴본 Code Behinde와 차이가 있음을 알 수 있다. DataContext는 XAML의 UI 요소의 바인딩 구문에 사용될 수 있는 데이터를 가리킨다. 예를 들어 x:Name이 “ID”인 TextBox의 Text는 LoginID라는 항목에 바인딩 되어 있는데 이것은 바로 DataContext로 지정한 LoginViewModel의 LoginID 프로퍼티를 가리키는 것이다. 이와 비슷하게 LoginPasswd는 LoginViewModel의 LoginPasswd 프로퍼티를 가리킨다. 앞에서 알아본 내용에 따르면 TextBox의 Text 속성은 기본값으로 TwoWay 바인딩을 사용하므로 LoginViewModel의 LoginID 값을 변경하면 변경 된 값이 TextBox의 Text속성에 반영이 되거나 또는 사용자가 TextBox에 직접 타이핑해 넣은 값이 LoginViewModel의 LoginID에 저장될 것을 기대할 수 있다.

using System.Windows;
using System.Windows.Input;
using WpfApplication1.ViewModels;

namespace WpfApplication1
{
    public partial class LoginWindow : Window
    {
        public LoginWindow()
        {
            InitializeComponent();

            this.Loaded += OnLoaded;
            this.LoginBtn.Click += LoginButtonClick;
            this.DataContext = new LoginViewModel();
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            Keyboard.Focus(this.ID);
        }

        private void LoginButtonClick(object sender, RoutedEventArgs e)
        {
            var viewModel = this.DataContext as LoginViewModel;
            if (string.IsNullOrEmpty(viewModel.LoginID))
            {
                MessageBox.Show("아이디를 입력해주세요.");
                Keyboard.Focus(this.ID);
                return;
            }
            if (string.IsNullOrEmpty(viewModel.LoginPasswd))
            {
                MessageBox.Show("비밀번호를 입력해주세요.");
                Keyboard.Focus(this.Passwd);
                return;
            }
            doLogin();
        }

        private bool doLogin()
        {
            //-- 로그인 처리
            var viewModel = this.DataContext as LoginViewModel;
            MessageBox.Show(string.Format("아이디={0}, 비밀번호={1}", viewModel.LoginID, viewModel.LoginPasswd));
            return true;
        }
    }
}

실제로 아이디와 비밀번호에 값을 입력하고 로그인 버튼을 클릭하면 아래 그림과 같은 결과를 확인할 수 있다. 즉, 바인딩 대상(UI 요소)의 변경이 바인딩 소스(ViewModel)로 올바르게 전파되고 있는 것을 확인한 것이다. 이러한 변경의 전파는 UpdateSourceTrigger 값으로 PropertyChanged를 사용하고 있기 때문에 TextBox의 Text 속성 값이 변경될 때마다 발생하게 된다.


그럼 반대로 바인딩 소스(ViewModel)의 변경이 바인딩 대상(UI 요소)으로 전파되는지도 확인해보자. 이를 위해 간단하게 로그인 버튼 옆에 자동입력 버튼을 하나 추가한다.


 <StackPanel Orientation="Horizontal">

   <Button x:Name="AutoBtn" Content="자동입력"/>

   <Button x:Name="LoginBtn" Content="로그인" Margin="8,0,0,0"/>

 </StackPanel>


그리고 Code Behinde에는 다음과 같은 자동입력 버튼을 클릭했을 때 실행 될 핸들러를 하나 추가한다.


 private void AutoButtonClick(object sender, RoutedEventArgs e)

 {

     var viewModel = this.DataContext as LoginViewModel;

     viewModel.LoginID = "myid2";

     viewModel.LoginPasswd = "mypassword2";

 }


애플리케이션을 다시 빌드하고 실행한다. 그리고 자동입력 버튼을 클릭한 다음 바로 로그인 버튼을 클릭해보자. 그러면 아래 그림과 같이 조금은 이상한 결과를 얻게 된다.



AutoButtonClick 핸들러를 통해 LoginViewModel의 LoginID와 LoginPasswd 프로퍼티의 값을 변경 했으나 변경의 전파가 UI 요소로 올바르게 이루어 지지 않았다는 것을 알 수 있다. (Alert 윈도우를 통해 LoginViewModel의 LoginID와 LoginPasswd 프로퍼티는 값이 올바르게 변경되었다는 것을 확인할 수 있다.)


사실 이것은 ViewModel을 구현하는데 있어서 INotifyPropertyChanged 인터페이스의 필요성을 설명하기 위해 의도한 결과이다. OneWay나 TwoWay 바인딩 방식에서 바인딩 소스(ViewModel)의 변화를 바인딩 대상(UI 요소)으로 올바르게 전파하기 위해서는 INotifyPropertyChanged 인터페이스 구현이 반드시 필요하다.

using System.ComponentModel;

namespace WpfApplication1.ViewModels
{
    public class LoginViewModel : INotifyPropertyChanged
    {
        private string _loginID;
        public string LoginID
        {
            get { return _loginID; }
            set
            {
                _loginID = value;
                OnPropertyUpdate("LoginID");
            }
        }

        private string _loginPasswd;
        public string LoginPasswd
        {
            get { return _loginPasswd; }
            set
            {
                _loginPasswd = value;
                OnPropertyUpdate("LoginPasswd");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyUpdate(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

INotifyPropertyChanged 인터페이스는 PropertyChangedEventHandler라는 이벤트 객체를 하나 포함하고 있는데, 이 이벤트 객체를 통해 프로퍼티 값이 변경되었다는 것을 UI 요소에 알리게 된다. 이제 다시 애플리케이션을 빌드하고 실행해보자. 그리고 자동입력 버튼을 클릭하면 아이디와 비밀번호 TextBox에 LoginViewModel을 통해 변경된 값이 반영되는 것을 확인할 수 있다.



참고

https://msdn.microsoft.com/ko-kr/library/ms752347(v=vs.110).aspx



저작자 표시 비영리 변경 금지
신고

'프로그래밍 > C#/WPF' 카테고리의 다른 글

WPF 데이터 바인딩의 기초 - 2  (0) 2015.06.21
WPF 데이터 바인딩의 기초 - 1  (1) 2015.06.16
Posted by devop