Subscribe to INotifyPropertyChanged for nested (child) objects
Asked Answered
S

7

39

I'm looking for a clean and elegant solution to handle the INotifyPropertyChanged event of nested (child) objects. Example code:

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

}

Focus on the BestFriend Property and it's value setter. Now I know that I could do this manually, implementing all steps described in the comments. But this is going to be a lot of code, especially when I'm planning to have many child properties implementing INotifyPropertyChanged like this. Of course they are not going to be always of same Type, the only thing they have in common is the INotifyPropertyChanged interface.

The reason is, that in my real scenario, I have a complex "Item" (in cart) object which has nested object properties over several layers (Item is having a "License" object, which can itself have child objects again) and I need to get notified about any single change of the "Item" to be able to recalculate the price.

Do you some good tips or even some implementation to help me to solve this?

Unfortunately, I'm not able/allowed to use post-build steps like PostSharp to accomplish my goal.

Satinwood answered 10/11, 2010 at 9:56 Comment(1)
AFAIK, most binding implementations don't expect the event to propagate in that fashion. You haven't changed the value of BestFriend, after all.Growl
S
25

since I wasn't able to find a ready-to-use solution, I've done a custom implementation based on Pieters (and Marks) suggestions (thanks!).

Using the classes, you will be notified about any change in a deep object tree, this works for any INotifyPropertyChanged implementing Types and INotifyCollectionChanged* implementing collections (Obviously, I'm using the ObservableCollection for that).

I hope this turned out to be a quite clean and elegant solution, it's not fully tested though and there is room for enhancements. It's pretty easy to use, just create an instance of ChangeListener using it's static Create method and passing your INotifyPropertyChanged:

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

the PropertyChangedEventArgs provide a PropertyName which will be always the full "path" of your Objects. For example, if you change your Persons's "BestFriend" Name, the PropertyName will be "BestFriend.Name", if the BestFriend has a collection of Children and you change it's Age, the value will be "BestFriend.Children[].Age" and so on. Don't forget to Dispose when your object is destroyed, then it will (hopefully) completely unsubscribe from all event listeners.

It compiles in .NET (Tested in 4) and Silverlight (Tested in 4). Because the code in seperated in three classes, I've posted the code to gist 705450 where you can grab it all: https://gist.github.com/705450 **

*) One reason that the code is working is that the ObservableCollection also implements INotifyPropertyChanged, else it wouldn't work as desired, this is a known caveat

**) Use for free, released under MIT License

Satinwood answered 18/11, 2010 at 19:33 Comment(5)
I took a liberty to wrap your gist into a nuget package: nuget.org/packages/RecursiveChangeNotifierKrawczyk
Thanks @LOST, hope it helps somebodySatinwood
That's a nice piece of code but it seems it ends up in a StackOverflowException if class A has a property of type B and type B has a property of type A or if you otherwise have some kind of circular references at some point.Bier
Actually to be more specific if A exposes a property of type ObservableCollection<B> and B exposes a property of type ObservableCollection<A>, when you setup the listeners for A and in the callback for CollectionChanged you call B.CollectionsOfAs(a) you get a StackOverflowException. I faced the issue while using DbContext.SaveChanges in EntityFramework.Bier
Thank you for reporting @Guillaume, actually I think many frameworks that do serialize etc. have problems breaking out of a potential Stack Overflow or alternatively, end up in an endless loop. This is old code and I'm wondering how many people still seem to be using it, feel free to enhance etc.Satinwood
B
17

I think what you're looking for is something like WPF binding.

How INotifyPropertyChanged works is that the RaisePropertyChanged("BestFriend"); must only be fored when the property BestFriend changes. Not when anything on the object itself changes.

How you would implement this is by a two step INotifyPropertyChanged event handler. Your listener would register on the changed event of the Person. When the BestFriend gets set/changed, you register on the changed event of the BestFriend Person. Then, you start listening on changed events of that object.

This is exactly how WPF binding implements this. The listening to changes of nested objects is done through that system.

The reason this is not going to work when you implement it in Person is that the levels can become very deep and the changed event of BestFriend does not mean anything anymore ("what has changed?"). This problem gets larger when you have circular relations where e.g. the best friend of your monther is the mother of your best fiend. Then, when one of the properties change, you get a stack overflow.

So, how you would solve this is to create a class with which you can build listeners. You would for example build a listener on BestFriend.FirstName. That class would then put an event handler on the changed event of Person and listen to changes on BestFriend. Then, when that changes, it puts a listener on BestFriend and listens for changes of FirstName. Then, when that changes, it sends raises an event and you can then listen to that. That's basically how WPF binding works.

See http://msdn.microsoft.com/en-us/library/ms750413.aspx for more information on WPF binding.

Bowing answered 10/11, 2010 at 11:13 Comment(3)
Thanks for your answer, I wasn't really aware of some problems you described. Currently, only unsubscribing from Events is causing some headache. For example, BestFriend could be set to null. Is it really possible to unsubscribe that way, without having sort of INotifyPropertyChanging implemented?Satinwood
When the listener (the object I describe) gets the first reference to Person, it copies the reference to BestFriend and registers a listener to that reference. If the BestFriend changes (e.g. to null), it first disconnects the event from the copied reference, copies the new reference (possibly null) and registers the event handler on that (if it's not null). The trick here is that you absolutely need to copy the reference to your listener instead of using the BestFriend property of Person. That should solve your problems.Bowing
Very nice, I will try to implement this and post solution when I'm done. +1 vote at least for you =)Satinwood
L
4

Interesting solution Thomas.

I found another solution. It's called Propagator design pattern. You can find more on the web (e.g. on CodeProject: Propagator in C# - An Alternative to the Observer Design Pattern).

Basically, it's a pattern for updating objects in a dependency network. It is very useful when state changes need to be pushed through a network of objects. A state change is represented by an object itself which travels through the network of Propagators. By encapsulating the state change as an object, the Propagators become loosely coupled.

A class diagram of the re-usable Propagator classes:

A class diagram of the re-usable Propagator classes

Read more on CodeProject.

Lithic answered 26/1, 2012 at 14:56 Comment(1)
+1 Thanks for your contribution, I'll surely take a closer lookSatinwood
H
1

I have been Searching the Web for one day now and I found another nice solution from Sacha Barber:

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

He created weak references within a Chained Property Observer. Checkout the Article if you want to see another great way to implement this feature.

And I also want to mention a nice implementation with the Reactive Extensions @ http://www.rowanbeach.com/rowan-beach-blog/a-system-reactive-property-change-observer/

This Solution work only for one Level of Observer, not a full Chain of Observers.

Humus answered 9/5, 2014 at 12:18 Comment(1)
Thanks for the update. Sacha's solution is obviously the most advanced, while I can remember mine to also work well, anyway it's a topic I haven't touched for a while now :)Satinwood
G
0

I wrote an easy helper to do this. You just call BubblePropertyChanged(x => x.BestFriend) in your parent view model. n.b. there is an assumption you have a method called NotifyPropertyChagned in your parent, but you can adapt that.

        /// <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; });
    }
Glossal answered 21/8, 2012 at 17:28 Comment(2)
I've tried adding this but I get The name 'Disposable' does not exist in this context. What is Disposable?Sawdust
Disposable is a concrete helper class in Reactive extensions that creates concrete objects implementing IDisposable with various behaviours. You can remove that code though and handle the event unhook explicitly if you don't want to learn the joys if IDisposable right now (although its worth the effort).Glossal
G
0

Check-out my solution on CodeProject: http://www.codeproject.com/Articles/775831/INotifyPropertyChanged-propagator It does exactly what you need - helps to propagate (in elegant way) dependent properties when relevant dependencies in this or any nested view models change:

public decimal ExchTotalPrice
{
    get
    {
        RaiseMeWhen(this, has => has.Changed(_ => _.TotalPrice));
        RaiseMeWhen(ExchangeRate, has => has.Changed(_ => _.Rate));
        return TotalPrice * ExchangeRate.Rate;
    }
}
Gilliette answered 27/5, 2014 at 8:32 Comment(0)
D
0

Please take a look at EverCodo.ChangesMonitoring. This is a framework to handle PropertyChanged and CollectionChanged events on arbitrary hierarchy of nested objects and collections.

Create a monitor to handle all change events of the object tree:

_ChangesMonitor = ChangesMonitor.Create(Root);
_ChangesMonitor.Changed += ChangesMonitor_Changed;

Do arbitrary modifications on the object tree (all of them will be handled):

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

Handle all events in one place:

private void ChangesMonitor_Changed(object sender, MonitoredObjectChangedEventArgs args)
{
    // inspect args parameter for detailed information about the event
}
Dinodinoflagellate answered 6/7, 2022 at 9:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.