2023년 11월 6일 월요일

2가지 예제를 통한 간단한 WPF 바인딩 원리


바인딩의 기본 개념은 위 그림과 같다. 
바인딩 타겟은 항상 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 에게 변경여부를 알려주기 때문이다.

댓글 없음:

댓글 쓰기