I am trying to build a dynamic data container that allows (some of) the dynamically added properties to be bound to WinForm elements. So far, when I bind a regular object property the binding works fine.
Sample:
public class CompileTimePropertiesDataContainer {
public string TestString = "Hello World";
}
and then binding within the form works fine:
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";
At this point the above sample works and assumes that the updates to the objects property is done on the UI thread. So now I need to implement an object that has dynamic properties (for resuability across this project and other projects).
So I have implemented the following class (replacing CompileTimePropertiesDataContainer above):
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
}
And implemented DynamicPropertyDescriptor as follows (to set up the property descriptor for dynamically added properties when using GetProperties() on the DataContainer:
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
}
In the code above, I have implemented INotifyPropertyChanged to meet the requirements of binding to the winforms control as I understand it, and defined the property descriptors for both the DataContainer and the dynamic properties it provides.
Now back to the sample implementation, I adjusted the object to be 'dynamic' and now the binding won't seem to '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";
and another note the 'event PropertyChangedEventHandler PropertyChanged' in the DataContainer object is null, the event is firing (confirmed through debugging), but because PropertyChanged is null (nothing listening for the event), its not updating.
I have a feeling that the problem lies with my implementation of ICustomTypeDescriptor in the DataContainer OR the DynamicPropertyDescriptor.
When setting up data binding to a property, framework invokes
AddValueChanged
method of thePropertyDescriptor
of that property. To provide two-way data binding, your property descriptor should override that method and subscribe forPropertyChanged
event of the component and callOnValueChanged
method of the property descriptor:Example
You can find a working implementation in the following repository: