订阅INotifyPropertyChanged以获取嵌套(子)对象的属性更改通知

39

我正在寻找一个干净而优雅的解决方案,用于处理嵌套(子)对象的INotifyPropertyChanged事件。示例代码:

public class Person : INotifyPropertyChanged {

  private string _firstName;
  private int _age;
  private Person _bestFriend;

  public string FirstName {
    get { return _firstName; }
    set {
      // Short implementation for simplicity reasons
      _firstName = value;
      RaisePropertyChanged("FirstName");
    }
  }

  public int Age {
    get { return _age; }
    set {
      // Short implementation for simplicity reasons
      _age = value;
      RaisePropertyChanged("Age");
    }
  }

  public Person BestFriend {
    get { return _bestFriend; }
    set {
      // - Unsubscribe from _bestFriend's INotifyPropertyChanged Event
      //   if not null

      _bestFriend = value;
      RaisePropertyChanged("BestFriend");

      // - Subscribe to _bestFriend's INotifyPropertyChanged Event if not null
      // - When _bestFriend's INotifyPropertyChanged Event is fired, i'd like
      //   to have the RaisePropertyChanged("BestFriend") method invoked
      // - Also, I guess some kind of *weak* event handler is required
      //   if a Person instance i beeing destroyed
    }
  }

  // **INotifyPropertyChanged implementation**
  // Implementation of RaisePropertyChanged method

}

关注BestFriend属性及其值的设置器。现在我知道可以手动实现,按照评论中描述的所有步骤进行操作。但这将是很多代码,特别是当我计划像此类一样实现许多子属性实现INotifyPropertyChanged时。当然,它们不总是相同的类型,唯一相同之处是INotifyPropertyChanged接口。

原因是,在我的真实场景中,我有一个复杂的“项目”(在购物车中)对象,该对象具有多层嵌套的对象属性(Item拥有一个“License”对象,该对象本身可以再次拥有子对象),我需要获得有关“项目”的任何单个更改的通知,以便能够重新计算价格。

您是否有一些好的提示或甚至实现来帮助我解决这个问题?

不幸的是,我不能使用像PostSharp这样的后构建步骤来完成我的目标。


据我所知,大多数绑定实现并不期望事件以那种方式传播。毕竟,您没有更改BestFriend的值。 - Marc Gravell
7个回答

25

由于我无法找到现成的解决方案,因此我根据Pieters(和Marks)的建议(谢谢!)进行了自定义实现。

使用这些类,您将收到关于深层对象树中任何更改的通知,这适用于任何实现INotifyPropertyChanged类型和实现INotifyCollectionChanged*的集合(显然,我正在使用ObservableCollection来实现)。

我希望这是一个相当清洁和优雅的解决方案,但它尚未完全测试,还有改进的空间。使用起来非常简单,只需使用其静态Create方法创建ChangeListener的实例并传递您的INotifyPropertyChanged即可:

var listener = ChangeListener.Create(myViewModel);
listener.PropertyChanged += 
    new PropertyChangedEventHandler(listener_PropertyChanged);
PropertyChangedEventArgs 提供了一个PropertyName,它始终是您对象的完整“路径”。例如,如果更改您的Person的“BestFriend”名称,PropertyName将为“BestFriend.Name”,如果BestFriend具有Children集合并且更改其年龄,则该值将为“BestFriend.Children [].Age”,以此类推。不要忘记在对象销毁时进行Dispose,然后它将(希望)完全取消订阅所有事件侦听器。
它在.NET中编译(已在4中测试)和Silverlight(已在4中测试)。由于代码分为三个类,因此我已将代码发布到 gist 705450,您可以在这里获取全部代码:https://gist.github.com/705450 *) 代码能够工作的一个原因是ObservableCollection也实现了INotifyPropertyChanged,否则它将不能按预期工作,这是一个已知的警告
**) 免费使用,根据MIT许可证发布

1
我冒昧将您的Gist打包成NuGet包:https://www.nuget.org/packages/RecursiveChangeNotifier/ - LOST
感谢@LOST,希望能对某些人有所帮助。 - thmshd
这是一段不错的代码,但如果类A具有类型为B的属性,并且类型B具有类型为A的属性,或者在某些时候您有某种循环引用,它似乎最终会导致StackOverflowException - Guillaume
更具体地说,如果A公开了一个类型为ObservableCollection<B>的属性,并且B公开了一个类型为ObservableCollection<A>的属性,当您为A设置侦听器并在CollectionChanged的回调中调用B.CollectionsOfAs(a)时,会出现StackOverflowException。我在使用EntityFramework的DbContext.SaveChanges时遇到了这个问题。 - Guillaume
感谢@Guillaume的报告,事实上我认为许多进行序列化等操作的框架都存在在潜在的堆栈溢出问题或者无限循环问题。这是旧代码了,我想知道还有多少人在使用它,欢迎对其进行改进等操作。 - thmshd

17

我认为你需要的是类似于WPF绑定的东西。

INotifyPropertyChanged 的工作方式是当属性 BestFriend 更改时,RaisePropertyChanged("BestFriend") 必须被触发。而不是在对象本身更改时。

实现这一点的方法是通过双步骤的 INotifyPropertyChanged 事件处理程序。您的侦听器将在 Person 的更改事件上注册监听器。当 BestFriend 被设置/更改时,您将在 BestFriend Person 的更改事件上注册。然后,您开始侦听该对象的更改事件。

这正是 WPF 绑定如何实现的。嵌套对象的更改侦听是通过该系统完成的。

如果在 Person 中实现它,原因是级别可能会变得非常深,BestFriend 的更改事件再也没有意义了(“什么已经改变了?”)。 当你有循环关系时,问题变得更大,例如,你母亲的最好朋友是你最好的朋友的母亲。 然后,当其中一个属性更改时,会出现堆栈溢出。

所以,解决这个问题的方法是创建一个类,您可以在其中构建侦听器。 您可以例如在 BestFriend.FirstName 上构建一个侦听器。该类将在 Person 的更改事件上放置一个事件处理程序,并侦听 BestFriend 的更改。 然后,当它更改时,它会在 BestFriend 上放置一个监听器,并侦听 FirstName 的更改。 然后,当它更改时,它引发一个事件,然后您可以侦听该事件。这基本上就是 WPF 绑定的工作方式。

有关 WPF 绑定的更多信息,请参见http://msdn.microsoft.com/en-us/library/ms750413.aspx


谢谢你的回答,我之前并没有意识到你所描述的一些问题。目前,只有从事件中取消订阅会引起一些麻烦。例如,BestFriend 可能被设置为 null。这种方式真的可以取消订阅吗,而不需要实现类似于 INotifyPropertyChanging 的东西吗? - thmshd
当监听器(我所描述的对象)第一次获取到Person的引用时,它会将引用复制到BestFriend并在该引用上注册一个监听器。如果BestFriend发生更改(例如变为null),它首先从复制的引用中断开事件,复制新的引用(可能为null),并在其上注册事件处理程序(如果它不是null)。这里的诀窍是绝对需要复制引用到您的监听器,而不是使用PersonBestFriend属性。这应该解决您的问题。 - Pieter van Ginkel
非常好,我会尝试实现这个并在完成后发布解决方案。至少给你+1票 =) - thmshd

4
有趣的解决方案Thomas。
我找到了另一个解决方案。它被称为Propagator设计模式。您可以在网络上找到更多信息(例如,在CodeProject上:Propagator in C# - An Alternative to the Observer Design Pattern)。
基本上,这是一种用于更新依赖网络中对象的模式。当需要将状态更改通过对象网络推送时,它非常有用。状态更改由对象本身表示,它通过传播器网络旅行。通过将状态更改封装为对象,传播器变得松散耦合。
重用的传播器类的类图: A class diagram of the re-usable Propagator classesCodeProject上阅读更多。

+1 感谢您的贡献,我一定会仔细查看。 - thmshd

1

我已经在网上搜索了一天,现在我发现了另一个不错的解决方案,来自Sacha Barber:

http://www.codeproject.com/Articles/166530/A-Chained-Property-Observer

他在Chained Property Observer中创建了弱引用。如果您想看另一种实现该功能的好方法,请查看文章。
我还想提到一个使用Reactive Extensions的不错实现@ http://www.rowanbeach.com/rowan-beach-blog/a-system-reactive-property-change-observer/ 此解决方案仅适用于一个观察者级别,而不是完整的观察者链。

感谢更新。Sacha的解决方案显然是最先进的,虽然我记得我的也能很好地工作,但这是我已经有一段时间没有接触的话题了 :) - thmshd

0
我写了一个简易的助手来完成这个任务。在你的父视图模型中,只需调用BubblePropertyChanged(x => x.BestFriend)即可。请注意,假设您的父类有一个名为NotifyPropertyChagned的方法,但您可以进行相应的修改。
        /// <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; });
    }

我尝试添加这个但是出现了“在此上下文中不存在'Disposable'的名称”的错误。Disposable是什么? - Scott Chantry
Disposable是Reactive扩展中的一个具体辅助类,它创建实现IDisposable接口的具体对象,并具有各种行为。如果您现在不想学习IDisposable的优点(尽管值得一试),则可以删除该代码并显式处理事件取消挂钩。 - DanH

0

请查看我在CodeProject上的解决方案: http://www.codeproject.com/Articles/775831/INotifyPropertyChanged-propagator 它完全符合您的需求-在相关依赖项更改时(以优雅的方式)帮助传播依赖属性,无论是在此视图模型中还是在任何嵌套的视图模型中:

public decimal ExchTotalPrice
{
    get
    {
        RaiseMeWhen(this, has => has.Changed(_ => _.TotalPrice));
        RaiseMeWhen(ExchangeRate, has => has.Changed(_ => _.Rate));
        return TotalPrice * ExchangeRate.Rate;
    }
}

0
请查看EverCodo.ChangesMonitoring。这是一个框架,用于处理任意嵌套对象和集合的PropertyChanged和CollectionChanged事件。
创建一个监视器来处理对象树中的所有更改事件:
_ChangesMonitor = ChangesMonitor.Create(Root);
_ChangesMonitor.Changed += ChangesMonitor_Changed;

对对象树进行任意修改(所有修改都将被处理):

Root.Children[5].Children[3].Children[1].Metadata.Tags.Add("Some tag");
Root.Children[5].Children[3].Metadata = new Metadata();
Root.Children[5].Children[3].Metadata.Description = "Some description";
Root.Children[5].Name = "Some name";
Root.Children[5].Children = new ObservableCollection<Entity>();

在一个地方处理所有事件:

private void ChangesMonitor_Changed(object sender, MonitoredObjectChangedEventArgs args)
{
    // inspect args parameter for detailed information about the event
}

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