1

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.

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
Aaron Murray
  • 1,920
  • 3
  • 22
  • 38
  • 2
    What is the purpose of `DataContainer`? Based on the last code example of how you will use it - you don't need this at all. Binding will work with dynamic - you can use any instance of any type as "component" if all instances have property `TestString` – Fabio Dec 09 '17 at 20:49
  • 1
    When setting up data binding to a property, framework invokes AddValueChanged method of the PropertyDescriptor of that property. Your property descriptor should override that method and subscribe for PropertyChanged event of the component and call OnValueChanged method of the property descriptor. – Reza Aghaei Dec 09 '17 at 23:03
  • 2
    @fabio yeah that was not how its going to be used, that was just a simple example to recreate the problem. – Aaron Murray Dec 10 '17 at 23:37
  • 1
    @Aaron Murray, is it possible to bind the dynamic component into a WPF control ? –  Apr 28 '20 at 14:22

1 Answers1

3

When setting up data binding to a property, framework invokes AddValueChanged method of the PropertyDescriptor of that property. To provide two-way data binding, your property descriptor should override that method and subscribe for PropertyChanged event of the component and call OnValueChanged method of the property descriptor:

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;
}

Example

You can find a working implementation in the following repository:

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • 1
    This is almost guaranteed to be the piece that I missed, however, on implementing and testing again as above I am now getting can not "bind to the property 'TestString' on the DataSource" error, which means I have changed something in the class along the way causing this error to come up. I have confirmed that the PropertyDescriptor (for 'TestString') does in fact exist before binding, – Aaron Murray Dec 10 '17 at 10:06
  • 2
    I've tested the solution and have got the expected result. I shared the example in a GitHub repository. – Reza Aghaei Dec 10 '17 at 15:52
  • 2
    Bingo, implementing 'ICustomTypeDescriptor' seems to be the key to the can not bind to the property exception, which I had included in my code (which was why it was sort of working originally, however in one of my many iterations of trying to get the class working over the past 2 days I removed it and forgot it was necessary... All is working fantastic now and your solution was definitely the key to what I was missing. – Aaron Murray Dec 10 '17 at 18:40
  • 2
    Great! I saw you have the implementation but didn't see you forget to declare the interface in implementation list! – Reza Aghaei Dec 10 '17 at 18:44
  • yeah that's what happens when you are googling all these different 'solutions' and trying to figure out which one meets your needs properly (some solutions solve different problems), so then you try different things and add and remove pieces along the way and accidentally take out something that you actually needed to keep in. Sometimes I get lost in trying to resolve problems in code that it gets out of control. :) – Aaron Murray Dec 10 '17 at 20:55
  • 1
    @RezaAghaei, when testing your GitHub implementation, I get several Binding exception in the debug console. –  Apr 28 '20 at 15:00