WPF에서 데이터 바인딩을 통해 개체를 참조하는 방식을 크게 세 가지 방식으로 제공하고 있습니다.
첫 번째는 이 글에서 알아볼 System.ComponentModel.CustomTypeDescriptor 와 System.ComponentModel.INotifyPropertyChanged 를 구현하여
리플렉션을 이용해 속성 특성을 검색하고 변경 알림을 사용하는 방법이고
두 번째는 System.ComponentModel.INotifyPropertyChanged 구현으로 특정 속성의 변경 알림만 구현해 주면 데이터 바인딩 엔진 자체에서 리플렉션을 사용하고
필요한 속성을 참조합니다.
이 경우 자체적으로 Lightweight Code Generator(LCG)를 사용해서 최적화 하기 때문에 첫 번째 방식 보다는 좀 더 나은 성능을 보입니다.
세 번째는 System.Windows.DependencyProperty 를 통한 바인딩 방식 입니다.
이 경우 데이터 바인딩 엔진은 리플렉션을 사용하지 않기 때문에 속도가 가장 빠릅니다.
이 글에서 다루는 코드는 다음 Repository에서 확인할 수 있습니다.
Code_check - CustomTypeDescriptorEx
이번 글에서는 System.ComponentModel.CustomTypeDescriptor 클래스를 사용해 런타임시 동적으로 속성 바인딩 하는 방법을 알아 봅니다.
바인딩 속성이 뷰 포트에 보여질때 리플렉션으로 해당 속성을 검색하고 접근하기 때문에 성능면에서는 좋지 않기 때문에 사실 이런 방식의 바인딩이 필요할지 의문이지만 이러한 방법으로 동적 바인딩
처리가 가능하다는 것만 알면 좋을 것 같습니다.
WPF는 데이터 바인딩의 소스가 System.ComponentModel.ICustomTypeDescriptor 인터페이스가 구현 되어 있는 경우 바인딩에 노출하는 속성 특성 컬렉션 System.ComponentModel.PropertyDescriptorCollection 을 요청합니다.
따라서 뷰 포트에 표시될 바인딩 속성이 있다면 WPF 데이터 바인딩 엔진은 System.ComponentModel.ICustomTypeDescriptor.GetProperties() 메서드를 호출하고 해당 메서드에서
바인딩 되는 속성들의 특성을 반환해 주면 동적 바인딩처리가 가능합니다.
이후엔 System.ComponentModel.PropertyDescriptor 클래스의 GetValue() 메서드로 노출될 바인딩 값을 가져와 렌더링 하게 되고 SetValue() 메서드로 값 변경처리를 하게 됩니다.
그럼 예제코드를 통해 살펴 보겠습니다.
먼저 System.ComponentModel.PropertyDescriptor 클래스는 추상 클래스므로 해당 클래스를 상속받는 Custom클래스를 다음과 같이 작성해 봅니다.
[CustomPropertyDescriptor.cs]
using System;
using System.ComponentModel;
namespace ICustomTypeDescriptorEx;
public class CustomPropertyDescriptor<T> : PropertyDescriptor
{
#region Member Fields
private Type _propertyType;
private Type _componentType;
private T? _propertyValue;
#endregion
#region Constructor
public CustomPropertyDescriptor(string propertyName, Type componentType)
: base(propertyName, new Attribute[] { })
{
_propertyType = typeof(T?);
_componentType = componentType;
}
#endregion
#region PropertyDescriptor Implementation Overriden Methods
public override bool CanResetValue(object component) { return true; }
public override Type ComponentType
{
get
{
return _componentType;
}
}
public override object? GetValue(object? component)
{
return _propertyValue;
}
public override bool IsReadOnly { get { return false; } }
public override Type PropertyType { get { return _propertyType; } }
public override void ResetValue(object component)
{
SetValue(component, default(T));
}
public override void SetValue(object? component, object? value)
{
if (value != null && value.GetType().IsAssignableFrom(_propertyType) == false)
{
throw new System.Exception("잘못된 타입입니다.");
}
_propertyValue = (T)value!;
}
public override bool ShouldSerializeValue(object component) { return true; }
#endregion
}
그리고 System.ComponentModel.CustomTypeDescriptor 를 사용해서 동적 데이터 바인딩의 기본이 되는 ModelBase를 다음과 같이 작성합니다.
[ModelBase.cs]
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
namespace ICustomTypeDescriptorEx;
public class ModelBase : CustomTypeDescriptor, INotifyPropertyChanged
{
#region Member Fields
List<PropertyDescriptor> _properties = new List<PropertyDescriptor>();
#endregion
#region Public Methods
/// <summary>
/// 속성 설정
/// </summary>
/// <typeparam name="T">속성 타입</typeparam>
/// <param name="propertyName">속성 이름</param>
/// <param name="propertyValue">속성 값</param>
public void SetPropertyValue<T>(string propertyName, T propertyValue)
{
var properties = this.GetProperties()
.Cast<PropertyDescriptor>()
.Where(prop => prop.Name.Equals(propertyName));
if (properties == null || properties.Count() != 1)
{
throw new Exception($"{propertyName} 속성이 없습니다.");
}
var property = properties.First();
property.SetValue(this, propertyValue);
OnPropertyChanged(propertyName);
}
/// <summary>
/// 속성 값 불러오기
/// </summary>
/// <typeparam name="T">속성 타입</typeparam>
/// <param name="propertyName">속성 이름</param>
/// <returns></returns>
public T? GetPropertyValue<T>(string propertyName)
{
var properties = this.GetProperties()
.Cast<PropertyDescriptor>()
.Where(prop => prop.Name.Equals(propertyName));
if (properties == null || properties.Count() != 1)
{
throw new Exception($"{propertyName} 속성이 없습니다.");
}
var property = properties.First();
return (T?)property.GetValue(this);
}
/// <summary>
/// 속성 추가
/// </summary>
/// <typeparam name="T">속성 타입</typeparam>
/// <typeparam name="U">속성 Onwer (Model)</typeparam>
/// <param name="propertyName"></param>
public void AddProperty<T, U>(string propertyName)
where U : ModelBase
{
var customProperty =
new CustomPropertyDescriptor<T>(
propertyName,
typeof(U));
_properties.Add(customProperty);
customProperty.AddValueChanged(
this,
(o, e) => { OnPropertyChanged(propertyName); });
}
#endregion
#region CustomTypeDescriptor Implementation Overriden Methods
public override PropertyDescriptorCollection GetProperties()
{
var properties = base.GetProperties();
return new PropertyDescriptorCollection(
properties.Cast<PropertyDescriptor>()
.Concat(_properties).ToArray());
}
#endregion
#region INotifyPropertyChange Implementation
public event PropertyChangedEventHandler? PropertyChanged = delegate { };
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChangedEventHandler? handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion INotifyPropertyChange Implementation
}
이제 위에서 만든 ModelBase클래스를 이용해서 동적 바인딩 처리가 가능합니다. 사용방법은 다음과 같습니다.
[DataModel.cs]
namespace ICustomTypeDescriptorEx;
public class DataModel : ModelBase
{
//public string? UserID
//{
// get;set;
//}
//public string? UserName
//{
// get; set;
//}
//public bool UserChecked
//{
// get; set;
//}
}
(데이터 바인딩 대상 클래스인 비어있는 DataModel 클래스)
[MainViewModel.cs]
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace ICustomTypeDescriptorEx;
public class MainViewModel
{
ObservableCollection<DataModel> _dataModelList = new ObservableCollection<DataModel>();
public MainViewModel() {
var data1 = this.InitDataModel();
var data2 = this.InitDataModel();
var data3 = this.InitDataModel();
var data4 = this.InitDataModel();
data1.SetPropertyValue<string>("UserID", Guid.NewGuid().ToString());
data1.SetPropertyValue<string>("UserName", "arong");
data1.SetPropertyValue<bool>("UserChecked", true);
data2.SetPropertyValue<string>("UserID", Guid.NewGuid().ToString());
data2.SetPropertyValue<string>("UserName", "ming");
data2.SetPropertyValue<bool>("UserChecked", true);
data3.SetPropertyValue<string>("UserID", Guid.NewGuid().ToString());
data3.SetPropertyValue<string>("UserName", "test1");
data3.SetPropertyValue<bool>("UserChecked", false);
data4.SetPropertyValue<string>("UserID", Guid.NewGuid().ToString());
data4.SetPropertyValue<string>("UserName", "test2");
data4.SetPropertyValue<bool>("UserChecked", false);
_dataModelList.Add(data1);
_dataModelList.Add(data2);
_dataModelList.Add(data3);
_dataModelList.Add(data4);
}
public ObservableCollection<DataModel> DataModelList
{
get { return _dataModelList; }
}
private DataModel InitDataModel()
{
DataModel dataModel = new();
dataModel.AddProperty<string, DataModel>("UserID");
dataModel.AddProperty<string, DataModel>("UserName");
dataModel.AddProperty<bool, DataModel>("UserChecked");
return dataModel;
}
private RelayCommand<Object>? _updatePropertyCommand;
public ICommand UpdatePropertyCommand
{
get
{
return _updatePropertyCommand ??
(_updatePropertyCommand = new RelayCommand<Object>(this.UpdatePropertyExecute));
}
}
private void UpdatePropertyExecute(object param)
{
DataModelList[1].SetPropertyValue<bool>("UserChecked", false);
}
}
MainViewModel에서는 DataModel 인스턴스를 생성하고 동적으로 바인딩 대상의 속성 특성을 추가합니다. 그리고 그렇게 추가된 DataModel의 컬렉션을 리스트뷰에 바인딩 처리 합니다.
[MainWindow.xaml]
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView Grid.Row="0"
ItemsSource="{Binding DataModelList}">
<ListView.View>
<GridView>
<GridViewColumn Width="100"
Header="UserID">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding UserID}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="100"
Header="UserName">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding UserName}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="100"
Header="UserChecked">
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding UserChecked}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<StackPanel Grid.Row="1">
<Button Content="Update property"
Height="30"
Command="{Binding UpdatePropertyCommand}"/>
</StackPanel>
</Grid>
버튼을 통해 특정 속성에 대한 값 변경시 System.ComponentModel.PropertyDescriptor 클래스의 SetValue() 메서드로 값이 할당되고 GetValue() 메서드로 값 변경된 값을 불러오는걸 확인해 볼 수 있습니다.
마찬가지로 “UserChecked”속성 변경 또한 체크박스 변경시 반영이 잘되는걸 볼 수 있습니다.
위 코드는 다음 Repository에서 확인할 수 있습니다.
Code_check - CustomTypeDescriptorEx