简体   繁体   English

为嵌套属性实现 INotifyPropertyChanged

[英]Implementing INotifyPropertyChanged for nested properties

I have a Person class:我有一个 Person 类:

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.对第一个文本框的更改将在 MyViewModel 中触发 OnPropertyChanged OnPropertyChanged( "Person" ) Great.伟大的。

Changes to the second text box ("Person.Address.StreetOne") does NOT fire OnPropertyChanged( "Person" ) inside MyViewModel.对第二个文本框("Person.Address.StreetOne")的更改不会在 MyViewModel 中触发 OnPropertyChanged OnPropertyChanged( "Person" ) Meaning it doesn't call the Person object's SET method.这意味着它不调用 Person 对象的 SET 方法。 Not great.不是很好。 Interestingly the SET method of StreetOne inside the Address class is called.有趣的是,Address 类中 StreetOne 的 SET 方法被调用了。

How do I get the SET method of the Person object inside the ViewModel to be called when Person.Address.StreetOne is changed???Person.Address.StreetOne更改时,如何获取 ViewModel 中 Person 对象的 SET 方法?

Do I need to flatten my data so SteetOne is inside Person and not Address??我是否需要展平我的数据,以便 SteetOne 在 Person 而不是 Address?

Thanks!谢谢!

While adding 'pass-through' properties to your ViewModel is a fine solution, it can quickly become untenable.虽然向 ViewModel 添加“传递”属性是一个很好的解决方案,但它很快就会变得站不住脚。 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.编辑:将处理程序移动到 c# 7 本地函数。

if you want the viewmodel SET to be called you could create a street property如果你想调用 viewmodel SET 你可以创建一个街道属性

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 xml

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

but this solution has its drawbacks.但这种解决方案有其缺点。 i go with Reeds answer in my projects我在我的项目中选择 Reeds 答案

How do I get the SET method of the Person object inside the ViewModel to be called when Person.Address.StreetOne is changed???当 Person.Address.StreetOne 更改时,如何获取 ViewModel 中 Person 对象的 SET 方法?

Why do you want to do this?你为什么要这样做? It should not be required - you only need the StreetOne property changed event to fire.它不应该是必需的 - 您只需要StreetOne属性更改事件。

Do I need to flatten my data so SteetOne is inside Person and not Address??我是否需要展平我的数据,以便 SteetOne 在 Person 而不是 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.您可以在 Person 类中订阅AddressPropertyChanged事件,并在Person更改时引发“Address”事件。 This shouldn't be necessary, however.然而,这不应该是必要的。

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

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

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 :它非常易于使用,只需使用它的静态Create方法创建一个ChangeListener实例并传递您的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. PropertyChangedEventArgs提供一个PropertyName ,它将始终是您的对象的完整“路径”。 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.例如,如果您更改 Persons 的“BestFriend”名称, PropertyName将为“BestFriend.Name”,如果BestFriend有一个 Children 集合并且您更改它的 Age,则该值将为“BestFriend.Children[].Age”等等。 Don't forget to Dispose when your object is destroyed, then it will (hopefully) completely unsubscribe from all event listeners.当您的对象被销毁时,不要忘记Dispose ,然后它将(希望)完全取消订阅所有事件侦听器。

It compiles in .NET (Tested in 4) and Silverlight (Tested in 4).它在 .NET(在 4 中测试)和 Silverlight(在 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 **因为代码分为三个类,我已将代码发布到gist 705450 ,您可以在其中获取所有内容: 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 *) 代码工作的一个原因是ObservableCollection还实现了INotifyPropertyChanged ,否则它不会按预期工作,这是一个已知的警告

**) Use for free, released under MIT License **) 免费使用,在MIT 许可下发布

There is a spelling mistake in your property change notification:您的属性更改通知中有拼写错误:

OnPropertyChanged( "SteetOne" );

should be应该

OnPropertyChanged( "StreetOne" );

Please take a look at EverCodo.ChangesMonitoring .请查看EverCodo.ChangesMonitoring This is a framework to handle PropertyChanged and CollectionChanged events on arbitrary hierarchy of nested objects and collections.这是一个框架,用于处理嵌套对象和集合的任意层次结构上的 PropertyChanged 和 CollectionChanged 事件。

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
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM