Динамическое связывание двухсторонних данных

Я пытаюсь создать динамический контейнер данных, который позволяет (некоторые из) динамически добавленных свойств быть привязанными к элементам 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";

На этом этапе приведенный выше пример работает и предполагает, что обновления свойства objects выполняются в потоке пользовательского интерфейса. Так что теперь мне нужно реализовать объект, который имеет динамические свойства (для повторяемости в этом и других проектах).

Поэтому я реализовал следующий класс (заменив 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 следующим образом (чтобы настроить дескриптор свойства для динамически добавляемых свойств при использовании GetProperties() в 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
}

В приведенном выше коде я реализовал INotifyPropertyChanged для удовлетворения требований привязки к элементу управления winforms, насколько я понимаю, и определил дескрипторы свойств как для DataContainer, так и для динамических свойств, которые он предоставляет.

Теперь вернемся к примеру реализации, я настроил объект как "динамический", и теперь привязка не будет "прилипать".

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

и еще одно замечание: "событие PropertyChangedEventHandler PropertyChanged" в объекте DataContainer имеет значение null, событие запускается (подтверждено отладкой), но поскольку PropertyChanged имеет значение null (ничего не прослушивает событие), его обновление не происходит.

У меня есть ощущение, что проблема заключается в моей реализации ICustomTypeDescriptor в DataContainer ИЛИ DynamicPropertyDescriptor.

1 ответ

Решение

При настройке привязки данных к свойству, каркас вызывает AddValueChanged метод PropertyDescriptor этой собственности. Чтобы обеспечить двустороннюю привязку данных, ваш дескриптор свойства должен переопределить этот метод и подписаться на 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;
}

пример

Работающую реализацию вы можете найти в следующем репозитории:

Другие вопросы по тегам