단순한 바인딩
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
댓글