.net 嵌套实现INotifyPropertyChanged的属性时,父对象必须传播更改吗?

.net 嵌套实现INotifyPropertyChanged的属性时,父对象必须传播更改吗?,.net,inotifypropertychanged,.net,Inotifypropertychanged,这个问题将表明我对实现/使用INotifyPropertyChanged时的预期行为缺乏了解: 问题是-要使绑定按预期工作,当您有一个本身实现INotifyPropertyChanged的类,该类具有INotifyPropertyChanged类型的嵌套属性时,是否希望您在内部订阅这些属性的更改通知,然后传播通知?或者,绑定基础架构是否需要智能使其变得不必要 例如(请注意,此代码不完整-只是为了说明问题): 在这个例子中,我们在Person对象中有一个嵌套的Address对象。两者都实现了INo

这个问题将表明我对实现/使用INotifyPropertyChanged时的预期行为缺乏了解:

问题是-要使绑定按预期工作,当您有一个本身实现INotifyPropertyChanged的类,该类具有INotifyPropertyChanged类型的嵌套属性时,是否希望您在内部订阅这些属性的更改通知,然后传播通知?或者,绑定基础架构是否需要智能使其变得不必要

例如(请注意,此代码不完整-只是为了说明问题):

在这个例子中,我们在Person对象中有一个嵌套的Address对象。两者都实现了INotifyPropertyChanged,因此对其属性的更改将导致向订阅者发送属性更改通知

但假设使用绑定,某人正在订阅Person对象上的更改通知,并且正在“侦听”Address属性的更改。如果地址属性本身发生更改(分配了不同的地址对象),它们将收到通知,但如果嵌套地址对象(城市或街道)包含的数据发生更改,它们将不会收到通知

这就引出了一个问题——绑定基础架构是否应该处理这个问题,或者我应该在Person的实现中订阅address对象上的更改通知,然后将它们作为对“address”的更改进行传播


如果你说到这里,感谢你花时间阅读这个冗长的问题。

当你说

…说某人是 订阅上的更改通知 一个人反对

某人正在订阅此人,并且无法知道地址是否已更改。
因此,您必须自己处理这种情况(这很容易实现)。

最简单的方法之一是向处理来自m_地址对象的通知事件的人员添加事件处理程序:

public class Person : INotifyPropertyChanged
{
   Address m_address;

   public Address
   {
      get { return m_address = value; }
      set
      {
         m_address = value;
         NotifyPropertyChanged(new PropertyChangedEventArgs("Address"));
         m_address.PropertyChanged += new PropertyChangedEventHandler( AddressPropertyChanged );
      }
   }
   void  AddressPropertyChanged( object sender, PropertyChangedEventArgs e )
   {
       NotifyPropertyChanged(new PropertyChangedEventArgs("Address"))
   }
}

如果您想让子对象看起来像是其父对象的一部分,您需要自己进行冒泡

例如,您将绑定到视图中的“Address.Street”,因此需要冒泡一个包含该字符串的notifypropertychanged

我写了一个简单的助手来做这件事。您只需在父视图模型构造函数中调用BubblePropertyChanged(x=>x.BestFriend)。n、 b.假设您的父级中有一个名为NotifyPropertyChanged的方法,但您可以根据需要进行调整

        /// <summary>
    /// Bubbles up property changed events from a child viewmodel that implements {INotifyPropertyChanged} to the parent keeping
    /// the naming hierarchy in place.
    /// This is useful for nested view models. 
    /// </summary>
    /// <param name="property">Child property that is a viewmodel implementing INotifyPropertyChanged.</param>
    /// <returns></returns>
    public IDisposable BubblePropertyChanged(Expression<Func<INotifyPropertyChanged>> property)
    {
        // This step is relatively expensive but only called once during setup.
        MemberExpression body = (MemberExpression)property.Body;
        var prefix = body.Member.Name + ".";

        INotifyPropertyChanged child = property.Compile().Invoke();

        PropertyChangedEventHandler handler = (sender, e) =>
        {
            this.NotifyPropertyChanged(prefix + e.PropertyName);
        };

        child.PropertyChanged += handler;

        return Disposable.Create(() => { child.PropertyChanged -= handler; });
    }
//
///将属性更改事件从实现{INotifyPropertyChanged}的子viewmodel冒泡到父viewmodel
///命名层次结构已就位。
///这对于嵌套视图模型很有用。
/// 
///作为实现INotifyPropertyChanged的viewmodel的子属性。
/// 
公共IDisposable BubblePropertyChanged(表达式属性)
{
//此步骤相对昂贵,但在安装过程中只调用一次。
MemberExpression body=(MemberExpression)property.body;
变量前缀=body.Member.Name+”;
INotifyPropertyChanged child=property.Compile().Invoke();
PropertyChangedEventHandler处理程序=(发件人,e)=>
{
this.NotifyPropertyChanged(前缀+e.PropertyName);
};
child.PropertyChanged+=处理程序;
返回一次性.Create(()=>{child.PropertyChanged-=handler;});
}

一个老问题,不过

我最初的方法是将更改的子属性附加到父属性。这有一个优点,即很容易使用父级的事件。只需要订阅家长

public class NotifyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    readonly Dictionary<string, AttachedNotifyHandler> attachedHandlers = new Dictionary<string, AttachedNotifyHandler>();

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected void AttachPropertyChanged(INotifyPropertyChanged notifyPropertyChanged,
        [CallerMemberName] string propertyName = null)
    {
        if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
        // ReSharper disable once ExplicitCallerInfoArgument
        DetachCurrentPropertyChanged(propertyName);
        if (notifyPropertyChanged != null)
        {
            attachedHandlers.Add(propertyName, new AttachedNotifyHandler(propertyName, this, notifyPropertyChanged));
        }
    }

    protected void DetachCurrentPropertyChanged([CallerMemberName] string propertyName = null)
    {
        if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
        AttachedNotifyHandler handler;
        if (attachedHandlers.TryGetValue(propertyName, out handler))
        {
            handler.Dispose();
            attachedHandlers.Remove(propertyName);
        }
    }

    sealed class AttachedNotifyHandler : IDisposable
    {
        readonly string propertyName;
        readonly NotifyChangedBase currentObject;
        readonly INotifyPropertyChanged attachedObject;

        public AttachedNotifyHandler(
            [NotNull] string propertyName,
            [NotNull] NotifyChangedBase currentObject,
            [NotNull] INotifyPropertyChanged attachedObject)
        {
            if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
            if (currentObject == null) throw new ArgumentNullException(nameof(currentObject));
            if (attachedObject == null) throw new ArgumentNullException(nameof(attachedObject));
            this.propertyName = propertyName;
            this.currentObject = currentObject;
            this.attachedObject = attachedObject;

            attachedObject.PropertyChanged += TrackedObjectOnPropertyChanged;
        }

        public void Dispose()
        {
            attachedObject.PropertyChanged -= TrackedObjectOnPropertyChanged;
        }

        void TrackedObjectOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
        {
            currentObject.OnPropertyChanged(propertyName);
        }
    }
}
然而,这种方法不是很灵活,而且无法控制,至少在没有额外复杂工程的情况下是如此。如果订阅系统具有遍历嵌套数据结构的灵活性,则其适用性仅限于第一级子级

尽管根据使用情况,这些警告可能是可以接受的,但我已经不再使用这种方法,因为永远无法确定最终将如何使用数据结构。目前更喜欢这样的解决方案:


这样,即使是复杂的数据结构也是简单且可预测的,用户可以控制如何订阅以及如何反应,它可以很好地利用绑定引擎的功能。

真的是这样吗?例如,在WPF中,我可以在这里这样做(如果我是正确的!),绑定基础设施将确保在地址属性更改或街道/城市更改时更新城市和街道文本框。很抱歉,xaml在评论中的表现不太好。无论如何,我想说的是,调用方(使用Person对象的实体)可能需要在Person对象和调用方使用的任何嵌套属性对象上注册更改通知。我不是说情况就是这样!。。。这就是为什么我问最初的问题,因为我相信有两种设计是可能的(要么将责任推到用户身上,要么推到实现者身上)。我试过查看MS文档,但没有找到任何确定的内容。干杯很抱歉,假设您正在使用Winfomrs。我对WPF没有太多的了解,但是,我的猜测是,在WPF中,它也将以完全相同的方式工作。WPF确实有冒泡事件的概念,您可能必须利用这一事实。看看这篇文章,它应该解释您如何创建自定义路由事件。感谢PK,我使用了路由事件和依赖项属性,但它们并不真正适用于我正在使用的内容。我正在为WPF应用程序使用MVVM方法(如果您使用一些WPF,强烈建议您研究它)
public class NotifyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    readonly Dictionary<string, AttachedNotifyHandler> attachedHandlers = new Dictionary<string, AttachedNotifyHandler>();

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected void AttachPropertyChanged(INotifyPropertyChanged notifyPropertyChanged,
        [CallerMemberName] string propertyName = null)
    {
        if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
        // ReSharper disable once ExplicitCallerInfoArgument
        DetachCurrentPropertyChanged(propertyName);
        if (notifyPropertyChanged != null)
        {
            attachedHandlers.Add(propertyName, new AttachedNotifyHandler(propertyName, this, notifyPropertyChanged));
        }
    }

    protected void DetachCurrentPropertyChanged([CallerMemberName] string propertyName = null)
    {
        if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
        AttachedNotifyHandler handler;
        if (attachedHandlers.TryGetValue(propertyName, out handler))
        {
            handler.Dispose();
            attachedHandlers.Remove(propertyName);
        }
    }

    sealed class AttachedNotifyHandler : IDisposable
    {
        readonly string propertyName;
        readonly NotifyChangedBase currentObject;
        readonly INotifyPropertyChanged attachedObject;

        public AttachedNotifyHandler(
            [NotNull] string propertyName,
            [NotNull] NotifyChangedBase currentObject,
            [NotNull] INotifyPropertyChanged attachedObject)
        {
            if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
            if (currentObject == null) throw new ArgumentNullException(nameof(currentObject));
            if (attachedObject == null) throw new ArgumentNullException(nameof(attachedObject));
            this.propertyName = propertyName;
            this.currentObject = currentObject;
            this.attachedObject = attachedObject;

            attachedObject.PropertyChanged += TrackedObjectOnPropertyChanged;
        }

        public void Dispose()
        {
            attachedObject.PropertyChanged -= TrackedObjectOnPropertyChanged;
        }

        void TrackedObjectOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
        {
            currentObject.OnPropertyChanged(propertyName);
        }
    }
}
public class Foo : NotifyChangedBase
{
    Bar bar;

    public Bar Bar
    {
        get { return bar; }
        set
        {
            if (Equals(value, bar)) return;
            bar = value;
            AttachPropertyChanged(bar);
            OnPropertyChanged();
        }
    }
}

public class Bar : NotifyChangedBase
{
    string prop;

    public string Prop
    {
        get { return prop; }
        set
        {
            if (value == prop) return;
            prop = value;
            OnPropertyChanged();
        }
    }
}