바인딩의 기본 개념은 위 그림과 같다.
바인딩 타겟은 항상 DependencyProperty 여야하고 바인딩 소스는 DependencyProperty 나 일반 프로퍼티(.NET 프로퍼티)나 상관없다.
여기서
OneWay 바인딩은 소스 -> 타겟
OneWayToSource 바인딩은 소스 <- 타겟
TwoWay 바인딩은 소스 <-> 타겟
으로 바인딩 된다.
1. TextBox 바인딩
화면을 위아래로 나눠서 상단에 텍스트 박스를 하단에 버튼 1개을 나타낸 간단한 UI를 만든다.
<Window x:Class="TextBoxBind.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:TextBoxBind"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50*"/>
<RowDefinition Height="50*"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0"
Text="{Binding InputText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}
" />
<Button x:Name="button" Content="Add Text" HorizontalAlignment="Left" Margin="42,29,0,0" Grid.Row="1" VerticalAlignment="Top" Click="button_Click"/>
</Grid>
</Window>
위 xaml 코드에서 TextBox는 InputText 프로퍼티와 바인딩을 하며 Mode는 TwoWay, UpdateSourceTrigger는 PropertyChanged 이다.
즉 양방향 바인딩이고 데이터 변환시 즉시 반영되는 바인딩이다.
code behind 는 아래와 같다.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace TextBoxBind
{
/// <summary>
/// MainWindow.xaml에 대한 상호 작용 논리
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private string inputText;
public string InputText
{
get { return inputText; }
set
{
if (inputText != value)
{
Console.WriteLine($"InputText : {value}");
inputText = value;
OnPropertyChanged("InputText");
}
}
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void button_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(new ThreadStart(() => InputText += "AAA"));
thread.IsBackground = true;
thread.Start();
}
}
}
Add Text 버튼클릭시 쓰레드로 InputText 프로퍼티의 값을 추가한다.
양방향 바인딩이므로 TextBox에 문자를 입력하면 InputText 값이 바뀌고 버튼을 클릭하면 TextBox 출력문자가 변경된다.('AAA'추가)
여기서 InputText의 setter 부분에 있는 OnPropertyChanged("InputText")
를 주석처리하면 버튼을 클릭해도 TextBox에 수정내용 즉 InputText 값이 반영되지 않는다.
이유는 바인딩을 하는 순간 TextBox는
public event PropertyChangedEventHandler PropertyChanged;
델러게이트 이벤트 핸들러를 구독하게되고 이벤트 발생시 InputText의 getter를 통해 현재 값을 가져와서 UI에 반영(Text 프로퍼티에)하기 때문이다.
Mode=OneWayToSource 를 하면 바인딩 타겟(TextBox) -> 바인딩 소스(InputText) 로만 데이터 전달이 되는데 이 경우는 TextBox가 바로 InputText의 setter 를 호출하게 되는데 이때는 OnPropertyChanged("InputText") 호출이 필요없다.(다른 이유에서 필요할 수 있으므로 호출하는것이 좋다)
2. DataGrid 바인딩
화면을 좌우로 나눠서 왼쪽에 DataGrid 를 오른쪽에 버튼과 기타 텍스트박스 컨트롤을 배치했다.
<Window x:Class="ListBindTest.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:ListBindTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<DataGrid Grid.Column="0" ItemsSource="{Binding People}" SelectedItem="{Binding SelectedItem}" SelectionMode="Single" />
<Canvas Grid.Column="1">
<TextBlock x:Name="textBlock" Canvas.Left="41" TextWrapping="Wrap" Text="Name" Canvas.Top="35"/>
<TextBlock x:Name="textBlock_Copy" Canvas.Left="41" TextWrapping="Wrap" Text="Age" Canvas.Top="87" HorizontalAlignment="Center" VerticalAlignment="Top"/>
<TextBox x:Name="textName" Canvas.Left="101" Text="{Binding SelectedItem.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Canvas.Top="35" Width="120"/>
<TextBox x:Name="textAge" Canvas.Left="101" Text="{Binding SelectedItem.Age, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Canvas.Top="87" Width="120" HorizontalAlignment="Center" VerticalAlignment="Top"/>
<TextBlock x:Name="textBlock_Copy1" Canvas.Left="41" TextWrapping="Wrap" Text="Name" Canvas.Top="183" HorizontalAlignment="Center" VerticalAlignment="Top"/>
<TextBlock x:Name="textBlock_Copy2" Canvas.Left="41" TextWrapping="Wrap" Text="Age" Canvas.Top="235" HorizontalAlignment="Center" VerticalAlignment="Top"/>
<Canvas x:Name="canvasTest">
<TextBox x:Name="textName_Copy" Canvas.Left="101" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Canvas.Top="183" Width="120" HorizontalAlignment="Center" VerticalAlignment="Top"/>
<TextBox x:Name="textAge_Copy" Canvas.Left="101" Text="{Binding Age, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Canvas.Top="235" Width="120" HorizontalAlignment="Center" VerticalAlignment="Top"/>
</Canvas>
<Button x:Name="button" Content="button" Canvas.Left="22" Canvas.Top="297" Click="button_Click"/>
<Button x:Name="button1" Content="button1" Canvas.Left="78" Canvas.Top="297" Click="button1_Click"/>
</Canvas>
</Grid>
</Window>
code behind 는 아래와 같다.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace ListBindTest
{
/// <summary>
/// MainWindow.xaml에 대한 상호 작용 논리
/// </summary>
public partial class MainWindow : Window
{
private MainViewModel viewModel = new MainViewModel();
private Person personTest = new Person();
public MainWindow()
{
InitializeComponent();
DataContext = viewModel;
personTest.Name = "Test";
personTest.Age = 100;
canvasTest.DataContext = personTest;
}
private void button_Click(object sender, RoutedEventArgs e)
{
Person p = new Person();
p.Name = personTest.Name;
p.Age = personTest.Age;
Thread thread = new Thread(new ThreadStart(() =>
{
this.Dispatcher.BeginInvoke(new Action(() =>
{
viewModel.People.Add(p);
}));
}));
thread.IsBackground = true;
thread.Start();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(new ThreadStart(() =>
{
Person p = viewModel.SelectedItem;
if (p != null) p.Age++;
}));
thread.IsBackground = true;
thread.Start();
}
}
public class Person : INotifyPropertyChanged
{
private string _name;
private int _age;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
public int Age
{
get { return _age; }
set
{
_age = value;
OnPropertyChanged(nameof(Age));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class MainViewModel : INotifyPropertyChanged
{
private ObservableCollection<Person> _people;
private Person _selectedItem;
public MainViewModel()
{
_people = new ObservableCollection<Person>();
_people.Add(new Person { Name = "Alice", Age = 25 });
_people.Add(new Person { Name = "Bob", Age = 30 });
_people.Add(new Person { Name = "Charlie", Age = 35 });
}
public ObservableCollection<Person> People
{
get { return _people; }
set
{
_people = value;
OnPropertyChanged(nameof(People));
}
}
public Person SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged(nameof(SelectedItem));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
button을 클릭하면 DataGrid 에 아이템이 추가되는데 여기서 주의해야할 점은 바인딩된 ObservableCollection 에 아이템을 추가할때 반드시 UI 쓰레드에서 추가해야한다는 점이다.
UI 쓰레드로 추가하지 않으려면 BindingOperations.EnableCollectionSynchronization 메소드를 사용하는 방법이 있다.
namespace ListBindTest
{
/// <summary>
/// MainWindow.xaml에 대한 상호 작용 논리
/// </summary>
public partial class MainWindow : Window
{
private MainViewModel viewModel = new MainViewModel();
private Person personTest = new Person();
public MainWindow()
{
InitializeComponent();
DataContext = viewModel;
personTest.Name = "Test";
personTest.Age = 100;
canvasTest.DataContext = personTest;
}
private void button_Click(object sender, RoutedEventArgs e)
{
Person p = new Person();
p.Name = personTest.Name;
p.Age = personTest.Age;
Thread thread = new Thread(new ThreadStart(() =>
{
#if false
this.Dispatcher.BeginInvoke(new Action(() =>
{
viewModel.People.Add(p);
//viewModel.People.Add(personTest);
}));
#else
viewModel.People.Add(p);
#endif
}));
thread.IsBackground = true;
thread.Start();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(new ThreadStart(() =>
{
//personTest.Name = personTest.Name + "X";
Person p = viewModel.SelectedItem;
if (p != null) p.Age++;
}));
thread.IsBackground = true;
thread.Start();
}
}
public class Person : INotifyPropertyChanged
{
private string _name;
private int _age;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
public int Age
{
get { return _age; }
set
{
_age = value;
OnPropertyChanged(nameof(Age));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class MainViewModel : INotifyPropertyChanged
{
private ObservableCollection<Person> _people;
private Person _selectedItem;
private object objLock = new object();
public MainViewModel()
{
_people = new ObservableCollection<Person>();
_people.Add(new Person { Name = "Alice", Age = 25 });
_people.Add(new Person { Name = "Bob", Age = 30 });
_people.Add(new Person { Name = "Charlie", Age = 35 });
BindingOperations.EnableCollectionSynchronization(_people, objLock);
}
public ObservableCollection<Person> People
{
get { return _people; }
set
{
_people = value;
OnPropertyChanged(nameof(People));
}
}
public Person SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged(nameof(SelectedItem));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
그리고 button1을 클릭하면 Age 값이 1증가하는데 이것은 Person 클래스가 INotifyPropertyChanged 를 구현해서 OnPropertyChanged(nameof(Age)) 를 호출하여 역시 1번 예제에서 설명한데로 public event PropertyChangedEventHandler PropertyChanged 를 구독중인 DataGrid 에게 변경여부를 알려주기 때문이다.