C#、WPF、MVVM 和 INotifyPropertyChanged

9

我有些困惑了,本以为自己理解了INotifyPropertyChanged。

我有一个小的WPF应用程序,前端是MainWindow类,中间是viewmodel,在后面是model。

在我的情况下,模型是Simulator类。

SimulatorViewModel几乎透明,只在MainWindow和Simulator之间接口属性。

Simulator实现了INotifyPropertyChanged,Simulator中的每个属性setter都调用RaisePropertyChanged方法:

private string serialNumber;
public string SerialNumber
{
    get { return serialNumber; }
    set
    {
        serialNumber = value;
        RaisePropertyChanged("SerialNumber");
    }
}

public event PropertyChangedEventHandler PropertyChanged;

public void RaisePropertyChanged(string propName)  
{  
    if (this.PropertyChanged != null)  
    {  
        this.PropertyChanged(this, new PropertyChangedEventArgs(propName));  
    }  
}    

在XAML中,我有一个带有以下绑定的文本框:
Text="{Binding Path=SerialNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"

DataContext是SimulatorViewModel(但请注意更改DataContext为模型的注释)。

ViewModel只是简单地传递属性:

public string SerialNumber  
{  
    get { return Simulator.SerialNumber; }  
    set { Simulator.SerialNumber = value; }  
}  

在模拟器中对属性SerialNumber进行程序化更新,但是奇怪的是,尽管在Simulator构造函数中设置了初始值,但这些更新并没有传播到UI。

如果我在Simulator中断SerialNumber setter,并跟随RaisePropertyChanged,我会发现PropertyChanged为null,因此事件不会向上传播到GUI。

这引出了几个问题:

  1. 应该将什么东西钩入PropertyChanged事件?我希望比“WPF”更具体。该事件与xaml中的Binding语句之间有何关联?

  2. 为什么初始属性值能够在启动时传播到UI,但后来却不能?

  3. Simulator(模型)是否正确实现了INotifyPropertyChanged,或者应该由ViewModel执行?如果ViewModel执行,那么模型中的程序化更改不会触发PropertyChanged,因此我对正确的模式不清楚。我意识到我的ViewModel几乎是多余的,但这是由于项目的简单性;一个更复杂的项目将使ViewModel概念更加有效。我的理解是,ViewModel是接口我的单元测试的地方。


你能否包含更新SerialNumber属性的代码? - devdigital
@devdigital - SerialNumber = GenerateSerialNumber(); - 我不认为那是问题所在。但我刚刚发现,如果我将DataContext设置为模拟器(模型),它就可以工作了。但这是否意味着ViewModel无关紧要? - Alan
你的SimulatorViewModel是什么样子的?从我所看到的,你的Simulator类已经扮演了ViewModel的角色。因此,你的SimulatorViewModel目前是多余的。你是如何将Simulator的通知传播到SimulatorViewModel的? - Goran
@Goran:为了更清晰,我在上面的代码中放了一个示例SimulatorViewModel属性。是的,在像这样简单的情况下,视图模型似乎是无关紧要的,但在更复杂的情况下,它会很有用。我认为你关于将模拟器通知传播到SimulatorViewModel是正确的。是DataContext启动了PropertyChanged事件的连接吗?这可以解释很多问题。但所有这些似乎都导致了一个问题:“ViewModel实际上有什么作用?” - Alan
2个回答

7
问题在于您在模型上引发了PropertyChanged事件,但是您的视图绑定到ViewModel。因此,您的View仅订阅ViewModel事件(而不是Model的事件)-这就是为什么文本框未更新的原因-因为它从未收到PropertyChanged事件。可能的解决方案之一是在ViewModel中监听Model PropertyChanged事件,并相应地引发同样的事件。
初始值被传播是因为您在ViewModel中的设置器/获取器是正确的,问题在于事件。
是的,您是正确的(请参见我的#1),应该在ViewModel上引发PropertyChanged,因为View绑定到它。这些事件不会在Model更改后自动触发,因此您应该在ViewModel中监听Model.PropertyChanged
最简单的脏修复方法是理解这个想法:
SimulatorViewModel(Simulator model)
{
    // this will re-raise Model's events on ViewModel
    // VM should implement INotifyPropertyChanged
    // method OnPropertyChanged should raise INPC for a given property
    model.PropertyChanged += (sender, args) => this.OnPropertyChanged(args.PropertyName);
}

是的,这一切都很有道理,谢谢。但是这个ViewModel让我开始感到困扰。我通过ViewModel(带有额外的代码)反射了所有的属性,正如你所说,我可能需要使用事件在ViewModel中监视模型更改(带有额外的代码):尽管我断言在一个更复杂的项目中它会很有用,但我开始怀疑是否需要ViewModel。什么样的东西真正应该放在ViewModel中,以证明需要一个中间层? - Alan
任何适用于您的视图但在模型中没有意义的属性或表示逻辑都应放在您的视图模型中。例如,您可能想在视图中显示用户的年龄,但是您的模型只有一个DateOfBirth属性。您可以使用诸如CSLA之类的框架构建丰富的模型,直接从视图绑定到这些模型,以节省模型->视图模型同步开销。 - devdigital
@Alan,ViewModels 可以有很多不同的用例。对我来说,最常见的用例是当 VM 应该是模型的编辑器时。在这种情况下,VM 将包含一些逻辑,如保存、回滚编辑所做的更改,在保存时显示进度条等等... 这是在编辑器上下文中具有很多意义的逻辑,但对于模型本身来说则没有意义。 - Snowbear
1
在WPF应用程序中,你几乎总是需要一个视图模型类。但你不一定需要一个单独的数据模型类。当你需要将功能分离成可重用和可测试的部分时,它变得有用,但如果你不需要这样做,拥有一个实现属性更改通知并封装数据模型的视图模型类就足够了。 - Robert Rossney
虽然我同意WPF在使用MVVM时摩擦较小,特别是因为社区已经采用了这种方式,但我认为WPF可以像WinForms一样轻松地用于所有代码后台风格的应用程序,无论是否建议这样做。使用分层还是使用不同的UI模式的问题实际上与WPF或任何特定的UI框架无关,应该独立理解和回答。 - jpierson

1
  1. 始终确保已设置为数据上下文的类的实例应实现INotifyPropertyChanged接口
  2. 在您的情况下,Simulator实现了INotifyPropertyChanged,但SimulatorViewModel被设置为DataContext,在这种情况下,UI只会知道SimulatorViewModel中是否有更改,而不是Simulator中的更改。

你可以这样做:

你可以在Simulator类中公开一些事件或委托,并钩入SimulatorViewModel类中的一些方法。然后,当Simulator类中的值发生更改时,调用事件或委托,该事件或委托将在SimulatorViewModel上执行,现在你可以从模拟器获取更新后的值并更新SimulatorViewModel中的属性。确保在两个类中使用不同的属性。


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