动态对象双向数据绑定
Dynamic object two way data binding
我正在尝试构建一个动态数据容器,它允许(部分)动态添加的属性绑定到 WinForm 元素。到目前为止,当我绑定一个常规对象时 属性 绑定工作正常。
样本:
public class CompileTimePropertiesDataContainer {
public string TestString = "Hello World";
}
然后在表单内绑定工作正常:
var component = new CompileTimePropertiesDataContainer();
lblTestString.DataBinding.Add(
"Text", component, "TestString", false, DataSourceUpdateMode.OnPropertyChanged);
// >>> lblTestString.Text == "Hello World"
component.TestString = "Another Sample";
// >>> lblTestString.Text == "Another Sample";
此时上面的示例工作并假定对对象 属性 的更新是在 UI 线程上完成的。所以现在我需要实现一个具有动态属性的对象(为了跨这个项目和其他项目的可重用性)。
所以我实现了以下 class(替换上面的 CompileTimePropertiesDataContainer):
public class DataContainer : DynamicObject, INotifyPropertyChanged
{
private readonly Dictionary<string, object> _data =
new Dictionary<string, object>();
private readonly object _lock = new object();
public object this[string name]
{
get {
object value;
lock (_lock) {
value = (_data.ContainsKey(name)) ? _data[name] : null;
}
return value;
}
set {
lock (_lock) {
_data[name] = value;
}
OnPropertyChanged(name);
}
}
#region DynamicObject
public override bool TryGetMember(GetMemberBinder binder, out object result) {
result = this[binder.Name];
return result != null;
}
public override bool TrySetMember(SetMemberBinder binder, object value) {
this[binder.Name] = value;
return true;
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged(
[CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
#region ICustomTypeDescriptor (DataContainer)
public AttributeCollection GetAttributes()
=> TypeDescriptor.GetAttributes(typeof(DataContainer));
public string GetClassName()
=> TypeDescriptor.GetClassName(typeof(DataContainer));
public string GetComponentName()
=> TypeDescriptor.GetComponentName(typeof(DataContainer));
public TypeConverter GetConverter()
=> TypeDescriptor.GetConverter(typeof(DataContainer));
public EventDescriptor GetDefaultEvent()
=> TypeDescriptor.GetDefaultEvent(typeof(DataContainer));
public PropertyDescriptor GetDefaultProperty()
=> TypeDescriptor.GetDefaultProperty(typeof(DataContainer));
public object GetEditor(Type editorBaseType)
=> TypeDescriptor.GetEditor(typeof(DataContainer), editorBaseType);
public EventDescriptorCollection GetEvents()
=> TypeDescriptor.GetEvents(typeof(DataContainer));
public EventDescriptorCollection GetEvents(Attribute[] attributes)
=> TypeDescriptor.GetEvents(typeof(DataContainer), attributes);
public PropertyDescriptorCollection GetProperties()
=> GetProperties(new Attribute[0]);
public PropertyDescriptorCollection GetProperties(Attribute[] attributes) {
Dictionary<string, object> data;
lock (_lock) {
data = _data;
}
// Add the dynamic properties from the class
var properties = data
.Select(p => new DynamicPropertyDescriptor(p.Key, p.Value.GetType()))
.Cast<PropertyDescriptor>()
.ToList();
// Include concrete properties that belong to the class
properties.AddRange(
TypeDescriptor
.GetProperties(GetType(), attributes)
.Cast<PropertyDescriptor>());
return new PropertyDescriptorCollection(properties.ToArray());
}
public object GetPropertyOwner(PropertyDescriptor pd) => this;
#endregion
}
并按如下方式实现 DynamicPropertyDescriptor(在 DataContainer 上使用 GetProperties() 时为动态添加的属性设置 属性 描述符:
public class DynamicPropertyDescriptor : PropertyDescriptor
{
#region Properties
public override Type ComponentType => typeof(DataContainer);
public override bool IsReadOnly => false;
public override Type PropertyType { get; }
#endregion
#region Constructor
public DynamicPropertyDescriptor(string key, Type valueType) : base(key, null)
{
PropertyType = valueType;
}
#endregion
#region Methods
public override bool CanResetValue(object component)
=> true;
public override object GetValue(object component)
=> ((DataContainer)component)[Name];
public override void ResetValue(object component)
=> ((DataContainer)component)[Name] = null;
public override void SetValue(object component, object value)
=> ((DataContainer)component)[Name] = value;
public override bool ShouldSerializeValue(object component)
=> false;
#endregion Methods
}
在上面的代码中,我已经实现了 INotifyPropertyChanged 以满足绑定到我理解的 winforms 控件的要求,并为 DataContainer 及其提供的动态属性定义了 属性 描述符。
现在回到示例实现,我将对象调整为 'dynamic',现在绑定似乎不会 'stick'。
dynamic component = new DataContainer();
// *EDIT* forgot to initialize component.TestString in original post
component.TestString = "Hello World";
lblTestString.DataBinding.Add(
"Text", component, "TestString", false, DataSourceUpdateMode.OnPropertyChanged);
// >>> lblTestString.Text == "Hello World"
component.TestString = "Another Sample";
// >>> lblTestString.Text == "Hello World";
另外请注意 DataContainer 对象中的 'event PropertyChangedEventHandler PropertyChanged' 为 null,事件正在触发(通过调试确认),但由于 PropertyChanged 为 null(没有监听事件),它不会更新。
我感觉问题出在我在 DataContainer 中实现 ICustomTypeDescriptor 或 DynamicPropertyDescriptor。
在设置到 属性 的数据绑定时,框架会调用 属性 的 PropertyDescriptor
的 AddValueChanged
方法。要提供双向数据绑定,您的 属性 描述符应该覆盖该方法并订阅组件的 PropertyChanged
事件并调用 属性 描述符的 OnValueChanged
方法:
void PropertyChanged(object sender, EventArgs e)
{
OnValueChanged(sender, e);
}
public override void AddValueChanged(object component, EventHandler handler)
{
base.AddValueChanged(component, handler);
((INotifyPropertyChanged)component).PropertyChanged += PropertyChanged;
}
public override void RemoveValueChanged(object component, EventHandler handler)
{
base.RemoveValueChanged(component, handler);
((INotifyPropertyChanged)component).PropertyChanged -= PropertyChanged;
}
例子
您可以在以下存储库中找到有效的实现:
我正在尝试构建一个动态数据容器,它允许(部分)动态添加的属性绑定到 WinForm 元素。到目前为止,当我绑定一个常规对象时 属性 绑定工作正常。
样本:
public class CompileTimePropertiesDataContainer {
public string TestString = "Hello World";
}
然后在表单内绑定工作正常:
var component = new CompileTimePropertiesDataContainer();
lblTestString.DataBinding.Add(
"Text", component, "TestString", false, DataSourceUpdateMode.OnPropertyChanged);
// >>> lblTestString.Text == "Hello World"
component.TestString = "Another Sample";
// >>> lblTestString.Text == "Another Sample";
此时上面的示例工作并假定对对象 属性 的更新是在 UI 线程上完成的。所以现在我需要实现一个具有动态属性的对象(为了跨这个项目和其他项目的可重用性)。
所以我实现了以下 class(替换上面的 CompileTimePropertiesDataContainer):
public class DataContainer : DynamicObject, INotifyPropertyChanged
{
private readonly Dictionary<string, object> _data =
new Dictionary<string, object>();
private readonly object _lock = new object();
public object this[string name]
{
get {
object value;
lock (_lock) {
value = (_data.ContainsKey(name)) ? _data[name] : null;
}
return value;
}
set {
lock (_lock) {
_data[name] = value;
}
OnPropertyChanged(name);
}
}
#region DynamicObject
public override bool TryGetMember(GetMemberBinder binder, out object result) {
result = this[binder.Name];
return result != null;
}
public override bool TrySetMember(SetMemberBinder binder, object value) {
this[binder.Name] = value;
return true;
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged(
[CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
#region ICustomTypeDescriptor (DataContainer)
public AttributeCollection GetAttributes()
=> TypeDescriptor.GetAttributes(typeof(DataContainer));
public string GetClassName()
=> TypeDescriptor.GetClassName(typeof(DataContainer));
public string GetComponentName()
=> TypeDescriptor.GetComponentName(typeof(DataContainer));
public TypeConverter GetConverter()
=> TypeDescriptor.GetConverter(typeof(DataContainer));
public EventDescriptor GetDefaultEvent()
=> TypeDescriptor.GetDefaultEvent(typeof(DataContainer));
public PropertyDescriptor GetDefaultProperty()
=> TypeDescriptor.GetDefaultProperty(typeof(DataContainer));
public object GetEditor(Type editorBaseType)
=> TypeDescriptor.GetEditor(typeof(DataContainer), editorBaseType);
public EventDescriptorCollection GetEvents()
=> TypeDescriptor.GetEvents(typeof(DataContainer));
public EventDescriptorCollection GetEvents(Attribute[] attributes)
=> TypeDescriptor.GetEvents(typeof(DataContainer), attributes);
public PropertyDescriptorCollection GetProperties()
=> GetProperties(new Attribute[0]);
public PropertyDescriptorCollection GetProperties(Attribute[] attributes) {
Dictionary<string, object> data;
lock (_lock) {
data = _data;
}
// Add the dynamic properties from the class
var properties = data
.Select(p => new DynamicPropertyDescriptor(p.Key, p.Value.GetType()))
.Cast<PropertyDescriptor>()
.ToList();
// Include concrete properties that belong to the class
properties.AddRange(
TypeDescriptor
.GetProperties(GetType(), attributes)
.Cast<PropertyDescriptor>());
return new PropertyDescriptorCollection(properties.ToArray());
}
public object GetPropertyOwner(PropertyDescriptor pd) => this;
#endregion
}
并按如下方式实现 DynamicPropertyDescriptor(在 DataContainer 上使用 GetProperties() 时为动态添加的属性设置 属性 描述符:
public class DynamicPropertyDescriptor : PropertyDescriptor
{
#region Properties
public override Type ComponentType => typeof(DataContainer);
public override bool IsReadOnly => false;
public override Type PropertyType { get; }
#endregion
#region Constructor
public DynamicPropertyDescriptor(string key, Type valueType) : base(key, null)
{
PropertyType = valueType;
}
#endregion
#region Methods
public override bool CanResetValue(object component)
=> true;
public override object GetValue(object component)
=> ((DataContainer)component)[Name];
public override void ResetValue(object component)
=> ((DataContainer)component)[Name] = null;
public override void SetValue(object component, object value)
=> ((DataContainer)component)[Name] = value;
public override bool ShouldSerializeValue(object component)
=> false;
#endregion Methods
}
在上面的代码中,我已经实现了 INotifyPropertyChanged 以满足绑定到我理解的 winforms 控件的要求,并为 DataContainer 及其提供的动态属性定义了 属性 描述符。
现在回到示例实现,我将对象调整为 'dynamic',现在绑定似乎不会 'stick'。
dynamic component = new DataContainer();
// *EDIT* forgot to initialize component.TestString in original post
component.TestString = "Hello World";
lblTestString.DataBinding.Add(
"Text", component, "TestString", false, DataSourceUpdateMode.OnPropertyChanged);
// >>> lblTestString.Text == "Hello World"
component.TestString = "Another Sample";
// >>> lblTestString.Text == "Hello World";
另外请注意 DataContainer 对象中的 'event PropertyChangedEventHandler PropertyChanged' 为 null,事件正在触发(通过调试确认),但由于 PropertyChanged 为 null(没有监听事件),它不会更新。
我感觉问题出在我在 DataContainer 中实现 ICustomTypeDescriptor 或 DynamicPropertyDescriptor。
在设置到 属性 的数据绑定时,框架会调用 属性 的 PropertyDescriptor
的 AddValueChanged
方法。要提供双向数据绑定,您的 属性 描述符应该覆盖该方法并订阅组件的 PropertyChanged
事件并调用 属性 描述符的 OnValueChanged
方法:
void PropertyChanged(object sender, EventArgs e)
{
OnValueChanged(sender, e);
}
public override void AddValueChanged(object component, EventHandler handler)
{
base.AddValueChanged(component, handler);
((INotifyPropertyChanged)component).PropertyChanged += PropertyChanged;
}
public override void RemoveValueChanged(object component, EventHandler handler)
{
base.RemoveValueChanged(component, handler);
((INotifyPropertyChanged)component).PropertyChanged -= PropertyChanged;
}
例子
您可以在以下存储库中找到有效的实现: