为嵌套(子)对象订阅 INotifyPropertyChanged

Posted

技术标签:

【中文标题】为嵌套(子)对象订阅 INotifyPropertyChanged【英文标题】:Subscribe to INotifyPropertyChanged for nested (child) objects 【发布时间】:2011-05-07 18:57:01 【问题描述】:

我正在寻找一种干净且优雅的解决方案来处理嵌套(子)对象的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 属性和它的值设置器。现在我知道我可以手动执行此操作,实现 cmets 中描述的所有步骤。但这将是很多代码,尤其是当我计划让许多子属性像这样实现INotifyPropertyChanged 时。当然,它们并不总是相同的类型,它们唯一的共同点是INotifyPropertyChanged 接口。

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

你有什么好的建议,甚至一些 实施帮助我解决 这个?

很遗憾,我无法/不允许使用 PostSharp 等后期构建步骤来实现我的目标。

【问题讨论】:

AFAIK,大多数绑定实现不期望事件以这种方式传播。毕竟,您没有更改BestFriend的值。 【参考方案1】:

我已经在网上搜索了一天,从 Sacha Barber 那里找到了另一个不错的解决方案:

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

他在 Chained Property Observer 中创建了弱引用。如果您想了解实现此功能的另一种好方法,请查看文章。

我还想提一下反应式扩展的一个很好的实现@ http://www.rowanbeach.com/rowan-beach-blog/a-system-reactive-property-change-observer/

此解决方案仅适用于一个级别的观察者,而不是完整的观察者链。

【讨论】:

感谢您的更新。 Sacha的解决方案显然是最先进的,虽然我记得我的也很好用,反正这是我有一段时间没接触过的话题了:)【参考方案2】:

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

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

我希望这是一个非常干净和优雅的解决方案,虽然它没有经过全面测试,并且还有改进的空间。它非常易于使用,只需使用它的静态Create 方法创建ChangeListener 的实例并传递您的INotifyPropertyChanged

var listener = ChangeListener.Create(myViewModel);
listener.PropertyChanged += 
    new PropertyChangedEventHandler(listener_PropertyChanged);

PropertyChangedEventArgs 提供一个PropertyName,它将始终是您的对象的完整“路径”。例如,如果您更改 Persons 的“BestFriend”名称,PropertyName 将是“BestFriend.Name”,如果 BestFriend 有一个 Children 集合并且您更改它的 Age,则值将是“BestFriend.Children[ ].年龄”等等。当你的对象被销毁时不要忘记Dispose,然后它会(希望)完全取消订阅所有事件监听器。

它在 .NET(在 4 中测试)和 Silverlight(在 4 中测试)编译。因为代码分为三个类,所以我将代码发布到 gist 705450,您可以在这里获取所有代码:https://gist.github.com/705450 **

*) 代码正常工作的一个原因是ObservableCollection 也实现了INotifyPropertyChanged,否则它将无法按预期工作,这是一个已知的警告

**) 免费使用,发布于MIT License

【讨论】:

我冒昧地将您的要点包装到一个 nuget 包中:nuget.org/packages/RecursiveChangeNotifier 感谢@LOST,希望对某人有所帮助 这是一段不错的代码,但如果类 A 具有 B 类型的属性并且 B 类型具有 @987654343 类型的属性,则它似乎以 ***Exception 结尾@ 或者如果您在某些时候有某种循环引用。 实际上更具体地说,如果A 公开了ObservableCollection<B> 类型的属性,而B 公开了ObservableCollection<A> 类型的属性,当您为A 设置侦听器时,在回调CollectionChanged 你调用B.CollectionsOfAs(a) 你得到一个***Exception。我在 EntityFramework 中使用DbContext.SaveChanges 时遇到了这个问题。 感谢您报告@G​​uillaume,实际上我认为许多进行序列化等的框架在突破潜在的堆栈溢出时遇到问题,或者最终陷入无限循环。这是旧代码,我想知道有多少人似乎仍在使用它,请随意增强等。【参考方案3】:

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

【讨论】:

【参考方案4】:

我写了一个简单的助手来做到这一点。您只需在父视图模型中调用 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 是 Reactive 扩展中的一个具体帮助器类,它创建实现 IDisposable 具有各种行为的具体对象。如果您现在不想学习 IDisposable 的乐趣,您可以删除该代码并明确处理事件 unhook(尽管它值得付出努力)。【参考方案5】:

有趣的解决方案托马斯。

我找到了另一个解决方案。它被称为传播者设计模式。您可以在网上找到更多信息(例如,在 CodeProject 上:Propagator in C# - An Alternative to the Observer Design Pattern)。

基本上,它是一种用于更新依赖网络中的对象的模式。当需要通过对象网络推送状态更改时,它非常有用。状态变化由穿过传播者网络的对象本身表示。通过将状态变化封装为一个对象,Propagators 变得松散耦合。

可重用的 Propagator 类的类图:

阅读更多CodeProject。

【讨论】:

+1 感谢您的贡献,我一定会仔细看看【参考方案6】:

我认为您正在寻找类似 WPF 绑定的东西。

INotifyPropertyChanged 的工作原理是,RaisePropertyChanged("BestFriend"); 必须在属性 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 的情况下以这种方式取消订阅吗? 当侦听器(我描述的对象)获得对Person 的第一个引用时,它会复制对BestFriend 的引用并将侦听器注册到该引用。如果BestFriend 更改(例如更改为null),它首先断开事件与复制的引用的连接,复制新的引用(可能是null)并在其上注册事件处理程序(如果不是null)。这里的诀窍是您绝对需要将引用复制到您的侦听器,而不是使用PersonBestFriend 属性。这应该可以解决您的问题。 非常好,我会尝试实现这个并在完成后发布解决方案。至少为你 +1 票 =)

以上是关于为嵌套(子)对象订阅 INotifyPropertyChanged的主要内容,如果未能解决你的问题,请参考以下文章

Rails 6嵌套形式茧不会保存子对象

CamelCase JSON WebAPI 子对象(嵌套对象、子对象)

避免在循环中使用嵌套的可观察订阅的正确方法是啥?

javascript - 如果在子对象中找到值,则搜索嵌套对象数组并返回父对象[重复]

JSON Stringify 忽略 Redis 发布上的嵌套对象

嵌套模式/子文档对象中的猫鼬 findById() - 聚合