实现嵌套属性的INotifyPropertyChanged

22
我有一个人类(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
}

我有一个Address类:

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
}

我有一个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" );
           }
     }

}

我有一个视图,其中包含以下行:

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

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

当视图加载时,两个值在文本框中都可以显示。

更改第一个文本框将在MyViewModel中触发OnPropertyChanged("Person")。太好了。

更改第二个文本框 ("Person.Address.StreetOne") 不会在MyViewModel中触发 OnPropertyChanged("Person")。这意味着它不会调用Person对象的SET方法。有趣的是,在Address类中StreetOne的SET方法被调用。

如何在更改Person.Address.StreetOne 时使位于ViewModel中的Person对象的SET方法得到调用?

我需要展开我的数据,使StreetOne位于Person而不是Address中吗?

谢谢!


1
@Revious...关于你的悬赏问题,已经有人回答了。那么你在这里想要什么样的答案呢? - Salah Akbari
1
@S.Akbari:被接受的答案是一个解决方法,它将属性展平了。我正在寻找一个真正的解决方案。 - Revious
1
@Revious 我添加了一个答案,使用事件处理程序从子级向父级传播更改的标准替代方案。我不确定为什么在原始答案中忽略了这一点,因为它是相当标准和“真正”的解决方案。 - Andrew Hanlon
1
@AndrewHanlon:谢谢! - Revious
5个回答

20

尽管将“pass-through”属性添加到您的ViewModel中是一个不错的解决方案,但它很快就会变得难以维护。标准替代方案是按以下方式传播更改:

  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");
        }
  }

现在更改通知从地址传播到个人。

编辑:将处理程序移动到C# 7本地函数中。


1
你的本地函数缺少返回类型吗? - DrEsperanto
1
在我的本地功能中加入了一个空的返回类型以满足编译器的要求后,这个工作得非常好。对于任何使用通用绑定基类的人,我能够通过一个新的方法进行适应,签名为:protected bool SetBindableField<T>(ref T field, T value, [CallerMemberName] string propertyName = null) where T : INotifyPropertyChanged,使其能够与我的基本可绑定类一起使用。使用 CallerMemberName 来获取属性名称是一个救星(当我使用复制粘贴方法测试时,可能会有多个不正确的参数名称错误)。 - DrEsperanto
1
最后一条评论:在将事件处理程序分配给字段之前,我不得不添加另一个检查 if (field != null)。在我的情况下,我确实将null分配给属性,因为有些情况下它是不需要的。我已经将嵌套对象映射到SQLite数据库中的单独表格中(使用EFCore,外键),并且当未使用时条目需要为空。通过这种更改,它可以正常工作。 - DrEsperanto

11

如果您想调用viewmodel的SET方法,您可以创建一个street属性。

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" );
       }
   }

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

但是这个解决方案也有其缺点。在我的项目中,我选择采用Reed的答案。


感谢您的回复。这种方法确实有效(我还不能点赞,否则我就会了)。我猜这样做或将我的“Person”类展平哪个选项更好,可能是一个争论的问题。我会选择Reeds的答案,但我不确定他所说的“你必须手动订阅PropertyChanged事件”的意思。我已经在Address、Person和MyViewModel层实现了OnPropertyChanged。 - lloyd christmas
最终我基本上选择了这条路线。虽然上面的方法确实可行,但是我没有将代码放在视图模型中,而是将其添加到了Person类中。所以基本上我有一个名为SreetOneDisplay的字符串。因此,我并没有真正地扁平化我的模型,仍然存在一个地址类。 - lloyd christmas

6
当Person.Address.StreetOne改变时,如何调用ViewModel中的Person对象的SET方法?
为什么你要这样做?这是不必要的 - 你只需要StreetOne属性更改事件被触发。
如果你想实际触发它,你不需要展开它(虽然这是一个选择)。你可以在Person类中订阅Address的PropertyChanged事件,并在Person内部更改时引发“Address”的事件。但是这不是必要的。

谢谢您的回复!让我来澄清一下。当更改“(Person.Address.StreetOne)”文本框时,MyViewModel内部不会发生任何事情。就像更改Person.Name时一样,我希望Person的SET方法能够触发。此外,即使调用Address类中SteetOne的SET方法,“OnPropertyChanged(“SteetOne”)”被调用时似乎仍然没有任何反应。Person类中PrimaryAddress的SET方法也没有被调用。 - lloyd christmas
1
@lloydchristmas 你为什么想要它被调用呢?WPF并不需要这个被调用。如果你想要它被调用,你就必须手动订阅PropertyChanged事件... - Reed Copsey
当调用 Person 的 SET 方法时,我会进行一些其他的验证调用。因此,每当 Person 更改时,我都会对其运行一些验证。例如,当 StreetOne 更改时,我需要验证它是否是有效的地址......类似的事情。那么,我该如何手动订阅 PropertChanged 事件呢?谢谢。 - lloyd christmas
1
@Reed,如果您想要在文档标题中实现类似Visual Studio的星号来表示文件有未保存的更改,该怎么办呢?您的顶层视图模型需要以某种方式获取信息,以便将更改传递给它。 - xr280xr

3

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

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

我希望这是一个相当干净和优雅的解决方案,尽管它没有经过充分测试,并且有改进的空间。它很容易使用,只需使用静态的Create方法创建ChangeListener实例并传递您的INotifyPropertyChanged即可:

var listener = ChangeListener.Create(myViewModel);
listener.PropertyChanged += 
    new PropertyChangedEventHandler(listener_PropertyChanged);
PropertyChangedEventArgs提供了一个PropertyName属性,它将始终是您对象的完整“路径”。例如,如果您更改了人员的“最好朋友”姓名,则PropertyName将是“BestFriend.Name”,如果BestFriend有一个孩子集合并且您更改了其年龄,则值将为“BestFriend.Children[].Age”,以此类推。不要忘记在对象被销毁时Dispose,这样它就会(希望)完全取消订阅所有事件监听器。

它可以在.NET(已测试4)和Silverlight(已测试4)中编译。由于代码分为三个类,我已将代码发布到gist 705450,您可以从中获取全部内容:https://gist.github.com/705450 **

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

**)免费使用,根据MIT许可证发布


1

您的属性更改通知中有一个拼写错误:

OnPropertyChanged( "SteetOne" );

应该是

OnPropertyChanged( "StreetOne" );


谢谢,我已经修复了。但这并不是问题的关键。其实这只是我举的一个例子。 - lloyd christmas
9
请使用 nameof(StreetOne) 代替固定字符串。 - Johan

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