当嵌套实现了INotifyPropertyChanged的属性时,父对象是否必须传播更改?

22

这个问题将展示我在实现/使用INotifyPropertyChanged时对预期行为的理解不足:

问题是 - 为了使绑定按预期工作,当您有一个类本身实现INotifyPropertyChanged,并且具有类型为INotifyPropertyChanged的嵌套属性时,您是否需要在内部订阅这些属性的更改通知,然后传播通知?或者绑定基础结构是否已经具备智能功能,使这样做变得不必要?

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

   public class Address : INotifyPropertyChanged
    {
       string m_street
       string m_city;

       public string Street
       {
          get { return m_street; }
          set
          {
             m_street = value;
             NotifyPropertyChanged(new PropertyChangedEventArgs("Street"));
          }
       }

       public string City
       {
          get { return m_city; }
          set 
          {
             m_city = value;
             NotifyPropertyChanged(new PropertyChangedEventArgs("City"));
          }
       }

    public class Person : INotifyPropertyChanged
    {
       Address m_address;

       public Address
       {
          get { return m_address = value; }
          set
          {
             m_address = value;
             NotifyPropertyChanged(new PropertyChangedEventArgs("Address"));
          }
       }
    }
所以,在这个例子中,我们有一个嵌套的Address对象在Person对象中。两者都实现了INotifyPropertyChanged接口,因此修改它们的属性将导致向订阅者传输属性更改通知。
但是假设使用绑定来订阅Person对象的更改通知,并正在“监听”Address属性的更改。如果更改了嵌套地址对象中包含的数据(例如城市或街道),则不会收到通知,只有当Address属性本身更改时(分配不同的Address对象)才会收到通知。
这引出了一个问题-绑定基础设施是否应该处理此问题,还是我应该在Person实现中订阅地址对象的更改通知,然后将其作为“Address”的更改传播?
如果你看到这里,感谢你抽出时间读完这个冗长的问题!

我在谷歌上搜索后找到了这个问题。对我来说,似乎你必须手动订阅子项的 PropertyChanged 事件并将其冒泡以使 WPF 绑定正常工作。 - loraderon
1
Loraderon,我非常确定那不是这样的 - 至少在我的测试中,已经证明了这一点。而且,没有任何信息(我所找到的)可以说明相反。你有任何链接或信息可以提供吗?谢谢。Phil - Phil
我也没有任何链接。在我的当前项目中,我不得不冒泡PropertyChanged事件才能使其工作。我是WPF和MVVM的新手,所以这可能只是我的项目中的一些特殊情况。 - loraderon
4个回答

3

其中最简单的方法是向Person添加事件处理程序,以处理来自m_address对象的通知事件:

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"))
   }
}

2
tkola,我知道如何为子项实现属性更改通知。问题是,这是否需要使数据绑定正常工作。从我所看到的情况来看,答案似乎是否定的——当子对象更改时,您不需要执行更改通知:它已经被绑定基础结构(至少对于WPF)有效处理了。 - Phil
10
在Address属性的setter方法中将m_address设置为新值之前,您可能希望先取消订阅旧m_address的PropertyChanged事件。 - Ben Gribaudo
如果地址是一个DependencyProperty呢? - tofutim

1
如果您想让子对象看起来像直接是其父对象的一部分,您需要自己进行冒泡。
对于您的示例,您将在视图中绑定到“Address.Street”,因此您需要冒泡一个包含该字符串的notifypropertychanged。
我编写了一个简单的帮助程序来完成这个操作。您只需在父视图模型构造函数中调用BubblePropertyChanged(x => x.BestFriend)。请注意,假设您的父级中有一个名为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; });
    }

1

当你说

...使用绑定时,意味着订阅了Person对象的更改通知,

你已经回答了这个问题。那个人正在订阅Person,并且无法知道Address是否发生了变化。 因此,您将不得不自己处理这种情况(这很容易实现)。


1
这是真的吗?例如在WPF中,我可以这样做:<StackPanel DataContext="{Binding thePerson}" Orientation="Vertical"> <StackPanel Orientation="Horizontal"> <Label Content="街道"/> <TextBox Text={Binding Address.Street}/> </StackPanel> <StackPanel Orientation="Horizontal"> <Label Content="城市"/> <TextBox Text={Binding Address.City}/> </StackPanel> </StackPanel> 在这里,(如果我没错!),绑定基础设施将确保如果Address属性或Street/City更改,则城市和街道文本框会更新。 - Phil
抱歉,XAML 在评论中的呈现效果不太好。无论如何,我想说的是,调用者(使用 Person 对象的实体)可能需要在 Person 对象和任何嵌套属性对象上注册更改通知,这些对象由调用者使用。我并不是说一定是这种情况!这就是为什么我问原始问题的原因,因为我相信有两种设计方法(将责任推给用户或实现者)。我尝试查看了微软文档,但没有找到明确的答案。干杯! - Phil
对不起,我假设你正在使用Winfomrs。我对WPF的了解不多,但我猜想在WPF中也是以同样的方式工作。WPF确实有事件冒泡的概念,你可能需要利用这一点。请参考这篇文章,它应该会解释如何创建自定义路由事件。 http://msdn.microsoft.com/en-us/library/ms742806.aspx - P.K
谢谢PK,我已经使用了路由事件和依赖属性,但它们并不适用于我正在使用的情况。我正在使用MVVM方法来开发WPF应用程序(如果你做一些WPF开发,强烈建议研究一下 - 它是一种特定的MVP变体,但由于WPF的数据驱动UI非常强大)。在MVVM方法中,有两种选择进行更改通知 - WPF依赖属性或INotifyPropertyChanged。通常最好使用Inotify,因为它适用于任何类(没有继承要求)。谢谢! - Phil

0

虽然这是一个老问题,但还是值得探讨的...

我的原始方法是将子属性更改附加到父级。这样做有一个优点,就是很容易消耗父级事件。只需要订阅父级即可。

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

然而,这种方法并不是非常灵活的,至少没有额外的复杂工程控制它。如果订阅系统具有遍历嵌套数据结构的灵活性,则其适用性仅限于第一级子项。

虽然可能会接受这些警告,具体取决于使用情况,但我已经放弃了这种方法,因为无法确定数据结构最终将如何使用。目前更喜欢像这样的解决方案:

https://github.com/buunguyen/notify

这样即使是复杂的数据结构也变得简单和可预测,订阅者可以控制如何订阅和如何反应,它与绑定引擎的能力相得益彰。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接