Implementing INotifyPropertyChanged for nested properties
Asked Answered
R

6

22

I have a Person class:

public class Person : INotifyPropertyChanged
{
     private string _name;
     public string Name{
     get { return _name; }
     set {
           if ( _name != value ) {
             _name = value;
             OnPropertyChanged( "Name" );
           }
     }

     private Address _primaryAddress;
     public Address PrimaryAddress {
     get { return _primaryAddress; }
     set {
           if ( _primaryAddress != value ) {
             _primaryAddress = value;
             OnPropertyChanged( "PrimaryAddress" );
           }
     }

     //OnPropertyChanged code goes here
}

I have an Address class:

public class Address : INotifyPropertyChanged
{
     private string _streetone;
     public string StreetOne{
     get { return _streetone; }
     set {
           if ( _streetone != value ) {
             _streetone = value;
             OnPropertyChanged( "StreetOne" );
           }
     }

     //Other fields here

     //OnPropertyChanged code goes here
}

I have a ViewModel:

public class MyViewModel
{
   //constructor and other stuff here

     private Person _person;
     public Person Person{
     get { return _person; }
     set {
           if ( _person != value ) {
             _person = value;
             OnPropertyChanged( "Person" );
           }
     }

}

I have a View which has the following lines:

<TextBox  Text="{Binding Person.Name, Mode=TwoWay,   
    UpdateSourceTrigger=PropertyChanged />

<TextBox  Text="{Binding Person.Address.StreetOne, Mode=TwoWay,   
    UpdateSourceTrigger=PropertyChanged />

Both values show up in the text box ok when the view loads.

Changes to the first text box will fire OnPropertyChanged( "Person" ) in MyViewModel. Great.

Changes to the second text box ("Person.Address.StreetOne") does NOT fire OnPropertyChanged( "Person" ) inside MyViewModel. Meaning it doesn't call the Person object's SET method. Not great. Interestingly the SET method of StreetOne inside the Address class is called.

How do I get the SET method of the Person object inside the ViewModel to be called when Person.Address.StreetOne is changed???

Do I need to flatten my data so SteetOne is inside Person and not Address??

Thanks!

Refinery answered 8/8, 2012 at 17:37 Comment(4)
@Revious ... Regarding your bounty the question has already been answered. So what kind of answer you are looking for here?Counterfactual
@S.Akbari: the accepted answer is a workaround which is flattening the property.. I was looking for a real solutionBilbao
@Bilbao I added an answer with the standard alternative which is to propagate changes from the child to the parent using an event handler. I am not sure how this was overlooked in the original answers since it is fairly standard and a 'real' solution.Agate
@AndrewHanlon: thanks!Bilbao
G
11

if you want the viewmodel SET to be called you could create a street property

public class MyViewModel
{
  //constructor and other stuff here
  public string Street{
    get { return this.Person.PrimaryAddress.StreetOne; }
    set {
       if ( this.Person.PrimaryAddress.StreetOne!= value ) {
         this.Person.PrimaryAddress.StreetOne = value;
         OnPropertyChanged( "Street" );
       }
   }

 }

xaml

<TextBox  Text="{Binding Street, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged />

but this solution has its drawbacks. i go with Reeds answer in my projects

Greedy answered 9/8, 2012 at 5:21 Comment(2)
Thanks for the response. This way does work (I can't up vote yet or I would). I guess then it's a debate whether doing this or flatting out my Person class is a better option. I would go with Reeds answer but I'm not sure what he means by "you'll have to subscribe to the PropertyChanged events manually". I already implement OnPropertyChanged on the Address, Person, and MyViewModel levels...Refinery
In the end I pretty much went this route. The above does work but instead of placing the code above in the view model, I added it to the Person class. So basically I had a sting called SreetOneDisplay. So I didn't really flatten my model, there is still an address class.Refinery
A
20

While adding 'pass-through' properties to your ViewModel is a fine solution, it can quickly become untenable. The standard alternative is to propagate changes as below:

  public Address PrimaryAddress {
     get => _primaryAddress;
     set {
           if ( _primaryAddress != value ) 
           {
             //Clean-up old event handler:
             if(_primaryAddress != null)
               _primaryAddress.PropertyChanged -= AddressChanged;

             _primaryAddress = value;

             if (_primaryAddress != null)
               _primaryAddress.PropertyChanged += AddressChanged;

             OnPropertyChanged( "PrimaryAddress" );
           }

           void AddressChanged(object sender, PropertyChangedEventArgs args) 
               => OnPropertyChanged("PrimaryAddress");
        }
  }

Now change notifications are propagated from Address to person.

Edit: Moved handler to c# 7 local function.

Agate answered 11/3, 2016 at 17:4 Comment(3)
Are you missing a return type on your local function?Socket
This worked great after I added a void return type to my local function to appease the compiler. For anyone using a generic Bindable base class I was able to adapt it to work with my base Bindable class through a new method with signature: protected bool SetBindableField<T>(ref T field, T value, [CallerMemberName] string propertyName = null) where T : INotifyPropertyChanged. Using CallerMemberName to get the property name is a life saver (I may have had more than one incorrect parameter name error when using a copy-paste approach to test this).Socket
One final comment: I had to add another check for if (field != null) before assigning the event handler to the field. In my case I do assign null to the property, as there are some cases where it is not needed. I have the nested objects mapped to separate tables in an SQLite database (using EFCore, foreign keys) and the entries need to be null when not used. It works fine with that change.Socket
G
11

if you want the viewmodel SET to be called you could create a street property

public class MyViewModel
{
  //constructor and other stuff here
  public string Street{
    get { return this.Person.PrimaryAddress.StreetOne; }
    set {
       if ( this.Person.PrimaryAddress.StreetOne!= value ) {
         this.Person.PrimaryAddress.StreetOne = value;
         OnPropertyChanged( "Street" );
       }
   }

 }

xaml

<TextBox  Text="{Binding Street, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged />

but this solution has its drawbacks. i go with Reeds answer in my projects

Greedy answered 9/8, 2012 at 5:21 Comment(2)
Thanks for the response. This way does work (I can't up vote yet or I would). I guess then it's a debate whether doing this or flatting out my Person class is a better option. I would go with Reeds answer but I'm not sure what he means by "you'll have to subscribe to the PropertyChanged events manually". I already implement OnPropertyChanged on the Address, Person, and MyViewModel levels...Refinery
In the end I pretty much went this route. The above does work but instead of placing the code above in the view model, I added it to the Person class. So basically I had a sting called SreetOneDisplay. So I didn't really flatten my model, there is still an address class.Refinery
I
6

How do I get the SET method of the Person object inside the ViewModel to be called when Person.Address.StreetOne is changed???

Why do you want to do this? It should not be required - you only need the StreetOne property changed event to fire.

Do I need to flatten my data so SteetOne is inside Person and not Address??

If you want to actually cause this to trigger, you don't need to flatten it (though that is an option). You can subscribe to the Address's PropertyChanged event within your Person class, and raise the event for "Address" within Person when it changes. This shouldn't be necessary, however.

Inhaul answered 8/8, 2012 at 17:43 Comment(4)
Thanks for the response! Let me try to clarify. Inside MyViewModel nothing happens when the '("Person.Address.StreetOne")' text box is changed. I would like the SET method of Person to fire just like it does when Person.Name is changed. Also, even though the SET method of StreetOne inside the Address class is called, nothing seems to happen when 'OnPropertyChanged( "SteetOne" )' is called. The SET method of PrimaryAddress inside the Person class is not called.Refinery
@lloydchristmas Why do you want it to be called, though? WPF doesn't require this to be called. If you want it to be called, you'll have to subscribe to the PropertyChanged events manually...Inhaul
I do some other validation calls when the SET method of Person is called. So anytime Person changes I run some validations on it. For example when StreetOne is changed I need to verify that it's a valid address... stuff like that. So how do I manually subscribe to the PropertChanged event? Thanks.Refinery
@Reed, how about if you want to implement an asterisk in the document header the way Visual Studio does to indicate the file has unsaved changes? Your top level view model would somehow need the info that something changed to bubble up to it.Verbatim
B
3

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

Bilbao answered 10/3, 2016 at 11:58 Comment(0)
T
1

There is a spelling mistake in your property change notification:

OnPropertyChanged( "SteetOne" );

should be

OnPropertyChanged( "StreetOne" );

Tenace answered 8/8, 2012 at 20:53 Comment(2)
Thanks, I fixed it. Not the problem though. Really this was just an example I made up.Refinery
Use nameof(StreetOne) instead of a fixed stringRufusrug
H
0

I can't comment, so I write here instead.

You wrote:

<TextBox Text="{Binding Person.Address.StreetOne, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged />

But shouldn't you have written:

<TextBox Text="{Binding Person.PrimaryAddress.StreetOne, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged />
Hospitable answered 25/4 at 10:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.