본문 바로가기
Windows/C#/WPF

데이터 바인딩 1

by hirudev 2021. 5. 9.

단순한 바인딩

using System.ComponentModel; 
using System.Runtime.CompilerServices; 
 
namespace DataBindingSample01 
{ 
    public class Person : INotifyPropertyChanged 
    { 
        public event PropertyChangedEventHandler PropertyChanged; 
 
        private void SetProperty<T>(ref T field, T value, [CallerMemberName]string propertyName = null) 
        { 
            field = value; 
            var h = this.PropertyChanged; 
            if (h != null) { h(this, new PropertyChangedEventArgs(propertyName)); } 
        } 
 
        private int age; 
 
        public int Age 
        { 
            get { return this.age; } 
            set { this.SetProperty(ref this.age, value); } 
        } 
 
        private string name; 
 
        public string Name 
        { 
            get { return this.name; } 
            set { this.SetProperty(ref this.name, value); } 
        } 
    } 

 

위와 같은 클래스 코드가 있다고 가정하고 아래와 같이 XAML 을 작성합니다.

 

<Window.Resources> 
    <local:Person x:Key="Person" Name="tanaka" Age="34" /> 
</Window.Resources> 

 

위 Resources 를 바인딩하기 위해 아래와 같이 작성합니다.

 

<TextBlock Text="{Binding Name, Source={StaticResource Person}}" /> 

 

Binding 의 처음에 지정하는 것은 Path Property 이며, Path 에는 프로퍼티 이름(위의 경우, Name) 을 지정합니다.

Binding의 Source가 지정되지 않은 경우, DataContext 프로퍼티가 자동으로 사용되어 아래와 같이도 작성이 가능합니다.

 

<Window.DataContext> 
    <local:Person Name="tanaka" Age="34" /> 
</Window.DataContext> 
<Grid> 
    <TextBlock Text="{Binding Name}" /> 
</Grid> 

 

Binding 의 모드

 

바인딩에는 수치 동기화 방법을 지정하기 위해 Mode 프로퍼티가 있습니다. Mode 프로퍼티의 수치를 아래와 같이 나타냅니다.

 

모드 설명
OneWay Source 에서 Target 으로 동기
TwoWay Source 와 Target 의 양방향 동기
OneWayToSource Target 에서 Source 로 동기
OneTime Source 에서 Target 에게 첫 1회만 동기.

 

Source 에서 Target 으로의 동기를 하기 위해선, Source 가 되는 오브젝트가 INotifyPropertyChanged 를 상속하고 있어야 합니다. Target에서 Source로의 동기는 특별히 상속해야할 인터페이스는 존재하지 않습니다.

Mode는 의존 관계 프로퍼티마다 기본값이 지정되어 있습니다. 일반적으로는 OneWay 가 지정되어 있어, TextBox의 Text 프로퍼티와 같은 양방향 동기가 필요한 것에 대해선 TwoWay가 지정됩니다. 아래와 같이 TextBlock 과 TextBox 를 Binding 하면, Person 오브젝트를 통해 TextBox 와 TextBlock 의 값이 동기됩니다.

 

<Window.DataContext> 
    <local:Person Name="tanaka" Age="34" /> 
</Window.DataContext> 
<StackPanel> 
    <TextBlock Text="{Binding Name}" /> 
    <TextBox Text="{Binding Name}" /> 
    <Button Content="TextBox에서 Focus 를 빼기 위한 용도" /> 
</StackPanel> 

 

상기와 같은 코드를 실행시키면, TextBox에서 Focus를 잃은 시점에 TextBlock와 TextBox 의 값이 동기화 됩니다.

이건 TextBox 가 Binding 되어 있는 값을 동기화 하는 타이밍이 Focus 를 벗어났을 타이밍이기 때문입니다.

이 움직임을 커스터마이즈를 하기 위해선 Binding의 UpdateSourceTrigger 프로퍼티를 지정합니다. UpdateSourceTrigger 프로퍼티는 아래와 같은 값을 설정합니다.

 

설명
LostFocus 포커스를 잃어버린 시점에서 소스 값을 변경합니다.
PropertyChanged 프로퍼티 값이 변한 시점에서 소스값을 변경합니다.
Explicit UpdateSource 메소드를 호출하여 명시적으로 Source 변환을 지시한 Source 만 변환합니다.

 

앞에서 TextBlock 과 TextBox 에 Person 오브젝트를 동기한 예에서 Textbox 의 Binding 을 아래와 같이 바꿔서 작성하면 TextBox에 입력한 수치가 바로 Person 오브젝트를 경유하여 TextBlock 에 반영됩니다.

 

<Window.DataContext> 
    <local:Person Name="tanaka" Age="34" /> 
</Window.DataContext> 
<StackPanel> 
    <TextBlock Text="{Binding Name}" /> 
    <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> 
    <Button Content="TextBox에서 Focus 를 빼기 위한 용도" /> 
</StackPanel> 

 

ElementName 으로 Source 지정

<TextBox x:Name="textBox" /> 
<TextBlock Text="{Binding Text, ElementName=textBox}" /> 

 

RelativeSource 으로 Source 지정

<TextBlock  
    Text="{Binding HorizontalAlignment, RelativeSource={RelativeSource Self}}"  
    HorizontalAlignment="Left"/> 

상대적 경로 기준을 자기자신(Self)로 두고 HorizontalAlignment 값을 바인딩하여 Text로 출력한다.

위 예의 경우, Text 에는 Left 가 표시된다.

 

<TextBlock 
    Text="{Binding Title, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" /> 

 

위 소스는 자신이 속해있는 윈도우를 타고올라가 지정한 형태와 일치하면 해당 오브젝트가 소스가 된다.

위 경우 TextBlock 가 속해있는 Window의 Title 값을 출력하게 된다.

 

 

 

그 외에, TemplatedParent 라는 TemplateBinding 와 같은 기능을 제공하는 방법도 있습니다.

TemplateBinding이 Oneway 반식에 반해

TemplatedParent 를 지정한 경우 TwoWay 등의 Binding을 지정하는 것도 가능하단 점이 다릅니다.

 

오류 검증

    public class Person: INotifyPropertyChanged, INotifyDataErrorInfo
    {
        // INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        private void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
        {
            field = value;
            var h = this.PropertyChanged;
            if (h != null)
                h(this, new PropertyChangedEventArgs(propertyName));
        }

        // INotifyDataErrorInfo
        private Dictionary<string, IEnumerable> errors = new Dictionary<string, IEnumerable>();
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
        private void OnErrorsChanged([CallerMemberName] string propertyName = null)
        {
            var h = this.ErrorsChanged;
            if (h != null)
            {
                h(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }
        public IEnumerable GetErrors(string propertyName)
        {
            IEnumerable error = null;
            this.errors.TryGetValue(propertyName, out error);

            return error;
        }
        public bool HasErrors
        {
            get { return this.errors.Values.Any(e => e != null); }
        }

        // Person
        private string name;
        public string Name
        {
            get { return this.name; }
            set
            {
                this.SetProperty(ref this.name, value);
                if (string.IsNullOrEmpty(value))
                {
                    this.errors["Name"] = new[] { "이름을 입력해주세요." };
                }
                else
                {
                    this.errors["Name"] = null;
                }
                this.OnErrorsChanged();
            }
        }

        private int age;
        public int Age
        {
            get { return this.age; }
            set
            {
                this.SetProperty(ref this.age, value);
                if(value < 0)
                {
                    this.errors["Age"] = new[] { "나이는 0 이상을 입력해야 합니다." };
                }
                else
                {
                    this.errors["Age"] = null;
                }
                this.OnErrorsChanged();
            }
        }
    }

 

    <Window.DataContext>
        <local:Person />
    </Window.DataContext>
    <StackPanel>
        <TextBox Text="{Binding Name}" Margin="2.5" ToolTip="{Binding (Validation.Errors)/ErrorContent, RelativeSource={RelativeSource Self}}"/>
        <TextBox Text="{Binding Age}" Margin="2.5" ToolTip="{Binding (Validation.Errors)/ErrorContent, RelativeSource={RelativeSource Self}}" />
    </StackPanel>

 


추가로 참고할만한 링크

https://docs.microsoft.com/ko-kr/dotnet/desktop/wpf/data/?view=netdesktop-5.0

 

데이터 바인딩 2

https://hirudev.tistory.com/26

 

'Windows > C#/WPF' 카테고리의 다른 글

커멘드  (0) 2021.05.12
데이터 바인딩 2  (0) 2021.05.11
리소스 & ControlTemplate  (0) 2021.05.08
스타일  (0) 2021.05.08
애니메이션  (0) 2021.05.08

댓글