MVVM # 第一集






4.96/5 (103投票s)
将扩展的 MVVM 模式用于实际的 LOB 应用程序:第一部分
本系列其他文章
第一部分
第二部分
第三部分
第四部分
引言
在我看来,MVVM 是开发应用程序的最佳方式,它既灵活——允许在不进行复杂重写的情况下更改 GUI,又允许在不依赖复杂宏的情况下测试客户端逻辑。
在本系列文章中,我将使用我称之为 MVVM# 的增强型 MVVM 模式,在 WPF 中展示一个小型应用程序。
那么,我的实现有什么不同之处呢?主要有以下几点:
- 消息跟踪:发送消息时,我们现在知道它是否已被处理。
- 可取消的消息:
ViewModel
可以阻止消息进一步向下传递。 - 简单易用的模态对话框窗口
- 无“主窗口”:一切皆为视图。
- 使用控制器:控制器控制应用程序。
- 易于使用的设计时数据,实现“Blendability”
- 使用
ViewData
将数据绑定到视图,以及使用ViewModel
将行为绑定到视图。
在本文中,我将介绍主题和想法,并解释一些不同之处。
在第二篇文章中,我将展示我如何使用 MVVM# 设置任何项目,创建基础类以开始进行特定于应用程序的开发。
在第三篇文章中,我将为第二篇文章创建的骨架添加足够的“肉”,从而得到一个可运行的应用程序,尽管它可能功能不多。
在第四篇文章中,我将完成应用程序,展示一个(小型)但功能齐全的应用程序,演示一些可用的功能。
背景
我一直在关注 MVVM 模式一段时间了,但一直没有机会用该模式开发实际应用程序。我下载并查看了大多数各种框架,甚至理解了一些!但是,我在学习新东西时不想使用框架——我想从头开始理解主题。所以我开始玩,开发自己的 MVVM 应用程序,并进行重构以克服遇到的许多障碍。
在本文中,我将描述一些促使我扩展 MVVM 模式的思考过程,并介绍该模式。
在接下来的系列文章中,我将通过在 VS2010 中使用 C# 从头开始开发一个应用程序来演示如何实现此模式。
关注点
我的天!外面的世界真是个雷区!
有太多的文章,从非常博学到新手都有,真的很难辨别好坏。我要感谢 WPF Disciples 社区,尤其是 Josh 和 Marlon,以及 Pete Hanlon,他总是耐心地回答我那些晦涩难懂的问题!也要感谢网络上所有知道或不知道地帮助过我开发这个系列的人。难道免费的帮助不是令人惊叹地多吗?希望这个系列能帮助我回馈一点。
历史
- 初稿:2011 年 3 月
MVVM
我想,最好先简要介绍一下我所理解的 MVVM。
Model (模型) View (视图) ViewModel (视图模型)

模型
根据 dictionary.com,“Model”(模型)的意思是“通常以缩略形式表示某物,以展示其结构或外观。”
在我们的例子中,Model
是一个描述现实世界事物的类。在业务系统中,它可能是 Customer
(客户)或 Supplier
(供应商),在游戏中,它可能是一艘宇宙飞船或一个怪物(是的,我知道,怪物不是现实世界中的事物——但你懂我的意思!)。重要的是,就 MVVM 而言,这些类本身并没有什么特别之处。它们可能具有各种功能,能够将自己保存到数据库,带有属性装饰,或者只是简单的、普通的类,带有几个属性。
这些是用于建模系统所使用数据的对象。
Model
类可能已经存在,你可能基于现有数据库定义它们,或者你可能从头开始定义它们以反映你正在开发的应用程序的需求。
ViewModel
ViewModel
是定义某个交互式视觉元素的*数据*和*功能*的类。它是 View
的模型。
需要注意的是,ViewModel
*不*描述视图*外观*。它描述视图*功能*,以及它向用户提供的信息。
关于 ViewModel
应该在多大程度上描述 View
的视觉方面,一直存在争论;例如,标签的措辞应该由 View 决定,还是 ViewModel
应该参与?在我看来,这完全取决于你和你正在工作的特定项目。有时你的标签会来自 VM(例如,因为你在进行本地化),有时设计师可能希望对描述性文本有更多控制权。在我的示例中,我假设基本上,如果它不在数据库中,那么就由设计师提供——这是一个单语言应用程序。
所以你的项目赞助商(又名老板)告诉你:“我们想显示Customer
(客户)的详细信息,并允许用户修改它们。”
这就是ViewModel
的规格!他没有说明姓名是否为 Tahoma Bold 字体,或者“State”(状态)选择是文本框还是组合框——他只是为 CustomerEditViewModel
定义了功能和数据要求。
在这里,我应该指出,关于这一点有不同的观点。一些 MVVM 的支持者认为 ViewModel
应该非常紧密地模拟 View 的视觉方面——例如,他们可能在 ViewModel
中拥有“System.Windows.Visibility ShowTransactions;
”这样的属性,而不是使用 Transactions.Count
属性配合转换器将 0
转换为 Visibility.Hidden
,将任何其他数字转换为 Visibility.Visible
。如果你对 WPF Disciples(或他们在 2008 年)是如何思考这个问题感兴趣,那么请看这里(需要登录 Google Groups)。
数据是 Customer
。要求是能够查看和修改 Customer
的属性。
重要的是,我们不希望我们的 View
直接“知道”我们的 Model
(在本例中是 Customer
类)。
为什么不呢?嗯,如果你的 customer
类发生了变化怎么办?你可能会因为一时兴起或出于需要,完全用 Entity Framework 或 nHibernate 重写后端。而且你的 Customer
类不应该以任何方式了解你应用程序的 GUI 方面。
在这方面,你可以将 ViewModel
视为翻译器——它接收一个 Customer
对象,并以一种对视图友好的方式处理该对象的映射。
视图
View
是应用程序用户将看到的视觉元素。在 WPF 中,这将是 UserControl
。重要的是,虽然 View
是 UserControl
,但 UserControl
不一定是 View
。
请记住,这会混淆一些人。如果你创建了一个 UserControl
,它*可能*是一个 View
,但也可能只是一个 UserControl
。所有 View
类都将(间接)继承自 UserControl
,但我们仍然可以在应用程序中使用“原始”的 UserControl
。
但我怎么知道呢?我听到你问。很简单。你应该从定义你的 ViewModel
开始——所以如果你有一个 ViewModel
来处理所需的功能,那么你需要创建一个 View
——如果这是一个属于 ViewModel
功能一部分的视觉功能,但它*没有* ViewModel
,那么它就是一个普通的 UserControl
。
例如,在上面描述的 CustomerEditViewModel
中,客户的地址将由用户显示和修改。我可能会决定将显示和修改封装在一个 UserControl
中,但这个 UserControl
将使用 CustomerEditViewModel
作为其数据源——而不是 CustomerAddressViewModel
,甚至不是 AddressViewModel
。
IMHO(在我看来),MVVM 圈中有一种谬论,认为开发人员应该避免在 View
的代码隐藏文件(即 view.xaml.cs 文件)中编写任何代码。事实是,开发人员应该避免在 View
的代码隐藏文件中编写任何*与 GUI 无关*的代码。
说实话,代码是由 XAML 生成的——那么我们为什么*不*应该编写自己的代码呢?“不要”这个规则很好地*提醒*开发人员不要在 View 中放置业务逻辑——但是如果你想在 GUI 端做某事,并且在 C# 中编写它很方便,那就为了上帝的份上在 C# 中编写吧!
例如——你可能将 Button
的 Command
属性绑定到 View
的 ViewModel
中的 ICommand
——这很合理,不需要代码隐藏,而且简洁整洁。但是,如果我想在 MouseOver
时做某事怎么办?当然,我可以尝试使用行为(Behaviors)或其他同样冗长的方法。但是,为什么不在我的代码隐藏中编写代码来处理 MouseOver
事件,通过调用 ViewModel
上的 Command
来实现呢?这只是用 C# 而不是 XAML 来编写。
代码隐藏中的两三行 C# 代码,而不是四十行 XAML 或几个额外的类?可维护性!
一切如何协同工作
MVVM 的想法是,我们对视图进行建模,并将实际的 View
与 ViewModel
分开。原则上,这允许我们仅通过更改 View
来更改 GUI——ViewModel
提供的功能仍然会被使用,但用户会获得不同的体验。显然,View
需要了解 ViewModel
——因为它将绑定到其属性,并向其发送命令。然而,ViewModel
应该(几乎)不了解 View
。
你现在可以将你的 View
交给设计师去处理。他们可以随心所欲地更改视图,只要它的数据源是 ViewModel
并且它适当地使用了 ViewModel
的功能。
因为我们这里讨论的是 WPF,所以我们将 ViewModel
用作 View
的 DataContext
。View
的每个元素都绑定到 ViewModel
上的属性。ViewModel
接收 Model
数据(我们的 Customer
),并将需要从 Model
修改的每个属性映射到其自己的 Observable
属性。
请注意——这些是Observable 属性。也就是说,我们 ViewModel
中将被我们 View
绑定的属性*需要*是 Observable
的——通过成为实现 INotifyPropertyChanged
的对象的一部分。这只意味着,当属性值发生更改时,我们会触发一个使用属性名称的事件——WPF 的绑定随后会处理更新绑定到该属性的任何内容。
一切如何“垮掉”
嗯,也许“垮掉”有点言过其实了!
在我看来,很多 MVVM 的实现方式存在一些不足。在摸索过程中,我形成了自己的“理想”模型,即我认为它如何能很好地运作。你可能不同意我的思考过程——或者你可能有不同的解决方案来解决同样的问题。即使什么都没有,我希望这个系列的文章能引发一些思考。
控制应用程序
当应用程序启动时,它需要某段代码,在某个地方启动它,显示一个初始窗口等。在大多数 WPF 应用程序中,有一个初始 WPF 窗口会被创建——并且大多数 MVVM 应用程序将其视为一个 View
,并创建一个适当的 ViewModel
,可能称为 MainWindowViewModel
。
我不同意这种方法。我不喜欢一个 View
某种程度上“特殊”的概念。我可能从显示一个 Customers
列表以供选择的想法开始——但设计师可能决定他们实际上想先看到一个 Customer
编辑表单。
因此,*我的*初始类是一个我称之为 Controller
的单例。它是一个非视觉类,负责控制应用程序。对于大型应用程序,可能存在多个 Controller
,并包含一些基本功能。但请注意,我不赞成每个 ViewModel
都有一个 Controller
的想法。
这个 Controller
负责
- 实例化
View
及其关联的ViewModel
- 处理数据请求
- 处理数据更新
- 处理与表示相关的任何逻辑流程(例如,在发生某些事件时打开特定的
View
)
所以,当应用程序启动时,单例 Controller
被实例化。它将自身的一个引用传递给每个 ViewModel
,允许 ViewModel
使用 Controller
提供的功能。
Controller
创建一个初始 View
及其 ViewModel
,并在一个 Window 中显示该 View
。
它创建的 View
*可能*有子 View
,这些子 View
在设计时进行定位——在这种情况下,ViewModel
将负责创建适当的子 ViewModel
(s)。
现在,Controller
仅仅处理来自一个或多个 ViewModel
的“执行操作”请求。
顺便说一句,控制器可以被设计成“推送”数据到 ViewModel
,或者 ViewModel
可以从 Controller
请求数据。有时这更多是个人偏好问题。在我的情况下,我倾向于根据情况使用这两种选项。这取决于你是否从以下角度思考:
C:“嘿!CustomerEditViewModel
!允许用户编辑这个 customer
,好吗?”
或
C:“嘿!CustomerEditViewModel
,用户想用你来编辑一个 Customer
。”
VM:“好的!哪一个,Controller
?.
C:“这个,请!.
进一步以我们的 Customer
维护示例为例。规格现在是:“向用户展示客户列表,用户可以选择一个进行修改。当他们选择了一个客户后,他们可以更改详细信息并保存它们。”
Controller
的工作是
- 实例化
CustomerSelectionViewModel
- 给
CustomerSelectionViewModel
一个Customers
集合来处理 - 实例化
CustomerSelectionView
并将其DataContext
设置为CustomerSelectionViewModel
- 显示
CustomerSelectionView
- 等待...
现在,用户在 View
中选择一个 customer
。这会向 ViewModel
发送一个 Command
。ViewModel
告知 Controller
已选择一个 Customer
(当然,还会告诉 Controller
是哪个 customer
)。然后 Controller
- 获取此特定
Customer
的数据(如果它还没有所有数据) - 实例化
CustomerEditViewModel
,并将其Customer
数据 - 实例化
CustomerEditView
并将其DataContext
设置为CustomerEditViewModel
- 显示
CustomerEditView
- 等待...
现在,用户在 View
中更改数据并单击保存按钮(或者,根据我们设计师的意愿,执行某些其他操作),这会向 ViewModel
发送一个“保存”命令。ViewModel
请求 Controller
保存 Customer
。然后 Controller
- 保存数据
- 发送一条
Message
,表示Customer
已保存
数据与功能
大多数 MVVM 示例中的 ViewModel
兼具功能(处理命令、检索和更新数据)和数据(可绑定到 Observable
属性)。理想情况下,每个类应该只有一个功能,而这些 ViewModel
却有两个。
我的解决方案是引入一个新类——ViewData
。
ViewModel
将有一个 ViewData
类型的属性。这个 ViewData
现在包含了所有绑定到 View
的 Observable
属性。(我在这里说“所有”,但实际上我指的是“大多数”。ViewData
对象将包含 ObservableProperties
,用于绑定到 View
的每一个*源自所使用数据*的数据。ViewModel
可能仍然会有额外的 ObservableProperties
,被 View
绑定,这些是*功能*所必需的。
例如,在本系列文章附带的项目中,CustomerSelectionViewModel
中有一个名为 StateFilter
的 ObservableProperty
,用于允许用户按 State
过滤列表。
想要一个customer
的“只读”视图以及一个Customer
编辑视图?同一个 ViewData
,不同的 ViewModel
。
ViewModel
现在更像是一个视图的模型——它很好地描述了我们的规范,不是吗?*“向用户展示客户列表,用户可以选择一个进行查看或修改。当他们选择了一个客户后,他们可以更改详细信息并保存它们。”* Customer
列表是我们的 ViewData
,ViewModel
处理获取列表和用户从列表中选择项目。当我们编辑一个 Customer
时,CustomerEditViewData
具有一个customer
所有可编辑字段的属性,而 CustomerEditViewModel
处理整个过程(例如,当用户单击保存时)。
顺便说一句,这有助于回答之前提到过的一个问题——ViewModel
何时应该向 View
提供面向 GUI 的属性而不是面向数据的属性?现在,我们有了一种分离这两种概念的方法。ViewData
对象应该是*纯粹*以数据为中心的。如果我们想向 View
提供面向视图的数据,但又不想处理转换器(Converters)等,那么 ViewModel
可以拥有被 View
绑定的属性。例如:
我们有 CustomerViewData
,它将 Customer
模型属性映射到 Observable
属性供 View
绑定,例如 CustomerFirstName
、CustomerSurname
、CustomerAddressLine1
等。
我们有一个 CustomerDisplayViewModel
,它“包含”一个 CustomerViewData
实例。
一切都很好,但是设计师问我们是否不能很好地格式化 Address
——设计师无法轻松处理(例如)空地址第二行的情况,即使使用 Converters
。
现在是时候向我们的 ViewModel
添加一个可观察属性了——string FormattedAddress
。我们的设计师可以直接绑定到此属性,并且将其与 ViewData
类分离使得很容易知道我们在看什么。
Windows
在我看来(哈哈!),View
是一个离散的视觉功能单元。它可能独自在一个 Window 中;它可能在一个 Window 中,停靠在 Window 的某个部分;它可能在一个模态对话框中;它可能在运行时加载;它可能在设计时定位。所以,我可能会设计一个用于从 Customers
列表中选择 Customer
的 View
,并将其设计为一个 Window。我的 ViewModel
获取要查看的 Customers
集合,并处理用户选择一个 customer
进行编辑时调用的命令。
一切都很好。
现在,老板走过来说:“不行!我们想要那个选择的东西和编辑东西在同一个窗口上。你知道的,有点像你给我看的那种 Visual Studio,右边有一个文件树,你可以双击编辑。”当然,你将 View
改成不再是 Window 而是 UserControl
——一个简单的改变。哦——而且你现在必须将那个 UserControl
放在一个 Window 上——所以那个新 Window 最好是一个新的 View
,带有一个 ViewModel
。
当然,当老板看到它时,他又改变主意了,你不得不把一切都改回来。
问。 为什么 View
会根据它是 UserControl
还是 Window 而有所不同?
答。 不*应该*有区别。
我想将*每个*视图设计成一个带有其 ViewModel
的 UserControl
。我想让我的 Controller
确定在任何特定时间,任何特定视图是否需要显示在模态窗口、非模态窗口或某个容器上的 UserControl
(当然,这最终会被包含在一个窗口中)。
所以——我将我的 Selection
设计成一个 UserControl
而不是一个 Window
。我的 Controller
将它显示在一个单独的 Window
中。当 Controller
处理“Customer Selected
”事件时,它会实例化 CustomerEditView
并将其显示在*它自己*的 Window
中。
当老板想要它们都在一个窗口中时,会创建一个新的 CustomerSelectAndEditView
,将两个现有的 View
放在其表面上,并将 Controller
更改为显示单个 CustomerSelectAndEditView
在一个 Window 中。对任何一个 View
或 ViewModel
都不需要更改。
当他改回主意时,同样,只有 Controller
需要改变。
当然,我说起来很简单——但是 Controller
*如何*将一个 View
显示在一个 Window
中?
我的解决方案是有一个基类 View
,所有 View
s 都继承自它,它具有将*自身*显示在现有容器或新窗口中(无论是否模态)所需的方法。
所以我的控制器可以这样说:“嘿,View,请在新的模态窗口中显示你自己!”让 View
去担心它将如何做到这一点。
视图模型间的通信
显然,有时会有两个 ViewModel
,其中一个的更改需要在另一个中得到反映。在我们Customer
选择/编辑的例子中,当编辑功能保存数据时,选择界面需要刷新。
我见过的许多 MVVM 模型都使用 Messenger
或 Mediator
单例模式。这相当不错(取决于具体实现),因为它允许 ViewModels
说“我想被告知是否有人从其他 ViewModel
发送此消息”,并且允许 ViewModels
以“即发即忘”模式发送消息。
因此,我们的 Customer Selection 可以要求在每次发送 Customer
Updated 消息时收到通知,而我们的 CustomerEdit
可以在每次更新 Customer
时发送 CustomerUpdated
消息。
但我认为 ViewModels
没有理由发送消息。当 ViewModel
想要执行一个函数时,它请求(单例)Controller
代为执行该函数。然后 Controller
可以处理发送消息。我发现这可以更好地控制消息传递,因为 Controller
可以更轻松地处理围绕 Message
的任何逻辑。
例如,CustomerEditViewModel
只知道用户更新了 Customer
的数据。它通知 Controller
,“Customer 123 已用此数据更新”。
Controller
比 CustomerEditViewModel
有一个巨大的优势——它还可以访问 CustomerSelectionViewModel
中当前可见的 Customer
集合(或者,如果我们希望它拥有,它*可以*拥有该信息)。因此,Controller
可以决定不发送 CustomerUpdated
消息——而只是刷新列表并通知 CustomerSelectionViewModel
——或者它可以使用逻辑来确定当前选定的项目*不需要*刷新——所以我们的 Controller
可以根据我们的实现要求发送 CustomerUpdated
消息,或者 CustomerListsNeedToBeRefreshed
消息,或者根本不发送任何消息。ViewModel
根本没有这个选项。
原则上,Controller
也可以处理消息的接收,使用 ViewModels
上的方法来施加其意志,但这又依赖于 Controller
对 ViewModel
的了解超出了我的期望。所以在我的世界里,ViewModels
订阅消息。

摘要
在本文中,我解释了我对 WPF MVVM 的理解,并列出了一些我遇到的问题。然后我提出了一些解决方案。在下一篇文章中,我将介绍一个应用程序的基础知识,以演示其中一些功能。