MVVM - 在 Model 或 ViewModel 中使用 PropertyChanged?

21

我已经学习了几个MVVM教程,发现可以两种方式实现。大多数使用ViewModel进行PropertyChanged(这也是我一直在做的),但我发现有一个教程是在Model中实现的。这两种方法都可行吗?如果可以,不同方法的优缺点是什么?


1
模型只应该具有应用程序的业务逻辑。最好将绑定属性放在ViewModel中,这将解耦业务实体和视图。 - Kurubaran
我想到了这一点。当我看到模型中的一个绑定示例时,我感到困惑。 - Jason D
1
@程序员。数据绑定基础设施已经提供了足够的“解耦”机制。将所有实体都包装在伪视图模型中只会导致内存泄漏、UI/数据不一致和其他微妙的错误。仅仅为了抽象而添加抽象是不明智的做法。 - Jonathan Allen
7个回答

40

微软的模式和实践、MVVM的发明者John Gossman以及我都不同意所选择的答案。

通常,模型实现了使其易于与视图绑定的工具。这通常意味着它通过INotifyPropertyChanged和INotifyCollectionChanged接口支持属性和集合更改通知。表示对象集合的模型类通常派生自ObservableCollection类,该类提供了INotifyCollectionChanged接口的实现。

—— Microsoft Patterns and Practices:http://msdn.microsoft.com/en-us/library/gg405484%28v=pandp.40%29.aspx#sec4

此时数据绑定就发挥作用了。在简单的例子中,视图直接向模型进行数据绑定。一些模型的部分仅通过单向数据绑定在视图中显示。可以将模型的其他部分直接双向绑定到数据控件上进行编辑。例如,模型中的布尔值可以数据绑定到复选框,或者字符串字段可以数据绑定到文本框。

—— MVVM的发明者John Gossman:http://blogs.msdn.com/b/johngossman/archive/2005/10/08/478683.aspx

我的文章:http://www.infoq.com/articles/View-Model-Definition


将“视图模型”仅包装一个模型并公开相同的属性列表是一种反模式。视图模型的工作是调用外部服务并公开这些服务返回的单个和多个模型。

原因:

  1. 如果直接更新模型,视图模型将不会知道触发属性更改事件。这会导致UI失去同步。
  • 这严重限制了在父子视图模型之间发送消息的选项。
  • 如果该模型拥有自己的属性更改通知,那么问题1和2就不成问题了。相反,您需要担心的是包装VM超出范围,但模型没有的内存泄漏。
  • 如果您的模型很复杂,有许多子对象,则必须遍历整个树并创建第二个对象图,以影响第一个对象图。这可能相当乏味且容易出错。
  • 包装的集合特别难以处理。每当某个东西(UI或后端)向集合插入或删除项目时,影子集合都需要更新以匹配。这种代码真的很难做对。
  • 这并不是说您永远不需要包装模型的视图模型。如果您的视图模型公开的属性与模型显着不同,并且不能仅使用IValueConverter进行覆盖,则包装视图模型是有意义的。

    您可能需要包装视图模型的另一个原因是您的数据类由于某种原因不支持数据绑定。但即便如此,通常最好只是创建一个正常的可绑定模型并从原始数据类中复制数据。

    当然,您的视图模型将具有特定于UI的属性,例如当前选择的集合中的哪个项目。


    2
    ViewModel 是调用仓库的关键,如果您正在使用它。如果没有,则直接调用外部服务。视图模型的设计不会改变。 - Jonathan Allen
    2
    作者“逃避”是因为他明白自己在说什么。你传播的观点是根本不需要视图模型,这是完全错误的。直接绑定到模型是特例,而不是正常情况。而这个问题是关于“该怎么做”。也许你应该重新阅读问题和我的回答。我以为阅读一些资料可以帮助你理解MVVM的工作原理,但看起来这是徒劳的。特别是如果需求变得更加复杂,就更需要良好的架构和设计。但如果你不在乎这些事情,我为什么要费心呢? - Mare Infinitus
    1
    如果你至少读过这篇文章,你可能会找到其中的核心部分。 "简单示例"实际上并不是生产代码。直接从那篇文章中可以看出:然而,在实践中,只有应用程序UI的一小部分可以直接绑定到模型。也许你没有那么多真实世界应用的经验,但是在我工作的公司和项目中,直接绑定到模型是一个简单的禁忌。但是,你开放MVVM初学者指南是一个非常好的开始。我很感激。 - Mare Infinitus
    1
    简单并不意味着“不适用于生产”。简单意味着这是大部分代码应该遵循的模式。 - Jonathan Allen
    2
    @JonathanAllen 谢谢。你提出了一些非常好的观点。我同意包装器 VM 是代码浪费,除了使项目更加复杂之外,对项目结构没有任何贡献。VM 应该负责管理视图逻辑,而不是深入到实际业务逻辑所在的模型中。在此基础上再添加包装器 VM 只会增加复杂性,没有任何好处。 - Farawin
    显示剩余10条评论

    14

    INotifyPropertyChanged(INPC)接口用于Binding

    因此,在平均情况下,您想在ViewModel中实现它。

    ViewModel用于将Model与您的View解耦,因此无需在Model中使用INPC,因为您不希望将Bindings绑定到您的模型。

    在大多数情况下,即使是较小的属性,您仍然有一个非常小的ViewModel

    如果您想要一个坚实的MVVM基础,您可能会使用某种MVVM Framework,如caliburn.micro。使用它将为您提供ViewModelBase(或这里的NotifyPropertyChangedBase),这样您就不必自己实现这些接口成员,并且可以直接使用NotifyOfPropertyChange(() => MyProperty),这样更容易、更少出错。

    更新 由于似乎有很多Windows Forms开发人员,这里有一篇优秀的文章,可以更深入地了解MVVM的作用: MSDN Magazine on MVVM

    我特别链接了有关数据模型的部分,这正是问题所在。


    Caliburn.micro和MVVM Light有什么区别?我尝试过一段时间,但并不是很喜欢它。 - Jason D
    我刚刚试用了Caliburn.Micro,但我仍然更喜欢之前使用的框架。 - Jason D
    @JonathanAllen,你刚刚错过了MVVM的一个核心点。如果你不相信我,请就此提出问题。 - Mare Infinitus
    4
    SO让我评论为什么我对这个答案进行了负投票:因为Jonathan Allen的答案实际上符合MPP。 - Thomas
    2
    我也不同意这个答案,并且出于同样的原因,我同意@JonathanAllen的看法。如果视图已经启动(由查看模型支持),并且模型从视图以外的其他地方发生更改(可能会收到更改模型属性的网络命令),则除非模型本身引发了属性更改,否则视图将不会同步。INotifyPropertyChanged不是WPF的一部分(它在System.ComponentModel中),也不知道绑定。实际上,WPF绑定使用INotifyPropertyChanged来实现其绑定机制 - 但它们是不同的概念。 - Todd Burch
    显示剩余9条评论

    5

    我完全同意Jonathan Allen的观点。

    如果您在“View-Model”中没有要添加的内容(命令,影响演示等的特定于视图的属性),那么我肯定会在模型中实现INotifyPropertyChanged,并直接公开它(如果可以 - “模型”可能不是您的)。这样做不仅会重复很多样板代码,而且保持两者同步非常麻烦。

    INotifyPropertyChanged不是一个特定于视图的接口,它只是像名称所示的那样 - 在属性更改时引发事件。 WinForms,WPF和Silverlight恰好支持它进行绑定 - 我肯定已经在非演示目的下使用它了!


    你没有ViewModel来拥有一个ViewModel。关键是当你需要时,你可以轻松地添加这样的行为,因为你在第一次做出正确的设计决策。在需要时实现良好的设计是一件大麻烦,而且成本远高于一开始就有一个小的viewmodel。唯一的例外是你没有那些“任何要添加的东西”,没有那个。推荐的方式(由深入了解设计的人推荐,而不是infoq的人)是在你的viewmodel中公开模型。 - Mare Infinitus
    2
    唉,不行。拥有视图模型的整个意义在于它让你将数据与对数据的外部操作分离开来。一旦你开始混合像“保存”这样的ICommand和像“名字/姓氏”这样的普通属性,你就离开了这种模式。内部操作,例如验证和计算属性(例如FullName)应该在模型中进行单元测试,但不应再做更多的事情。 - Jonathan Allen
    3
    但是我为什么要费心呢?如果我有一个显示只读数据网格的视图,其中包含“项”,那么将该网格绑定到 ObservableCollection<Item> 比刻意创建一个 ItemViewModel 包装它、编写代码同步更改底层项目以触发适当的 PropertyChanged 事件、同步底层集合到 ViewModel 集合等要简单得多。在这种情况下,直接将模型绑定到视图会更简单-甚至是 Prism 指导文档所说的:http://msdn.microsoft.com/en-us/library/gg405484%28v=pandp.40%29.aspx#sec4 - Charles Mager
    现在我重新阅读了你说的话,我不确定你是否同意我刚才上面写的内容 - 但是这很难理解! - Charles Mager
    1
    就我而言,我同意你所说的,@CharlesMager。 - Jonathan Allen
    @CharlesMager:请看一下这个问题:https://dev59.com/CWkv5IYBdhLWcg3wqimS,我在这个主题上开始了一个新的问题:https://dev59.com/rXLYa4cB1Zd3GeqPdvl3。 - Mare Infinitus

    2
    MVVM 的创造者 JohnGossman 在他的 this 博客文章中(由 @Jonathan Allen 提到)指出: “在简单的例子中,视图直接数据绑定到模型。模型的一部分通过单向数据绑定简单地显示在视图上。模型的其他部分可以通过将控件双向绑定到数据来进行编辑。例如,模型中的布尔值可以与复选框数据绑定,字符串字段可以与文本框数据绑定。” “然而,在实践中,只有应用程序 UI 的一个小子集能够直接与模型进行数据绑定,特别是如果模型是一个现有的类或数据模式,应用程序开发人员对其没有控制权。”
    我更喜欢遵循那些在应用程序扩展时仍然适用的实践。如果“在实践中[...],只有一小部分应用程序UI可以直接绑定到模型”,那么这似乎不是一个好的实践,因为我不打算解决“简单情况”或“应用程序UI的一小部分”。对于“简单情况”,我甚至不会使用MVVM。

    实际上,当应用程序在复杂性方面扩展时,始终创建单独的ViewModel的坚持会暴露其缺点。我曾这样做。结果是必须同步两个平行层次结构的对象,非常头疼。另一方面,我还不知道一个理想的答案。需要以某种方式将与视图相关的属性(例如“selected”)与模型关联起来,同时能够保证一个ViewModel集合和相应的模型集合彼此同步。 - ToolmakerSteve

    1
    作为经验法则,任何你将要绑定的对象(即使你不需要双向绑定和属性更改通知),都必须实现INotifyPropertyChanged。这是因为未能这样做可能导致内存泄漏

    1

    所有被视图使用的类型都应该实现INotifyPropertyChanged接口(当然,如果它只有常量值则除外)。

    你是否将模型(而不是视图模型)返回给视图?如果是,那么它应该实现INotifyPropertyChanged接口。


    常量值很棘手。从API设计的角度来看,您不希望公开该接口并使消费者感到困惑。但是,数据绑定架构中的缺陷意味着您可能仍然想这样做。有关更多信息,请参见@HighCore的链接。 - Jonathan Allen

    1

    虽然我通常赞成模型实现INPC,但在复合视图模型中调用INPC的原因是它公开了可绑定到视图的推断属性。在我看来,由于INPC已经被嵌入到System.dll中,实现它的模型可能被认为是POCO。对于集合,基于模型的INPC有性能优势。在64位平台上,与ObservableCollection<Model>相比,包装器VM的字节大小会有8倍的乘数(加载SOS调试器扩展以获取实际大小)。


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