MVPVM 设计模式

用于 WPF 的 Model-View-Presenter-ViewModel 设计模式

Bill Kratochvil

下载代码示例

在我参与过的所有成功项目中,大多数成功的项目都具有一个共同的结果,即: 应用程序变得越大,代码库就变得越小。 表面上看,这似乎是矛盾的,但敏捷环境中的编码本身就有助于缩小代码库。 当需求发生变化时,将会进行重构,并且这种重构机会与后见之明相结合,能够更有效地重用现有组件,同时消除过程中的重复代码。

相比之下,有一些庞大的项目本来已被认定是成功的,但随着源代码的不断增长,几乎没有重用的机会。 这些项目变得非常耗费资源,为未来增长带来了风险。 那么这两者的主要区别是什么? 答案就是所使用的基础结构设计模式。 您所使用的模式将决定您是否会身陷困境或者错过未来可能带来的广阔机会。

在本文中,我将介绍这样一种模式,这种模式称为 Model-View-Presenter-ViewModel (MVPVM) 模式;由于 Model-View-ViewModel (MVVM) 模式的盛行,前者被许多 Windows Presentation Foundation (WPF) 开发人员所忽略。 这种企业应用程序设计模式是在 Microsoft 模式与实践方案的 Prism 项目 (Prism.CodePlex.com) 中引入的。 (注意: 这种模式还没有被正式命名,所以我将它称为 MVPVM。) 在 Prism 网站简介的一段摘录中,对 Prism 进行了恰如其分的描述:

“Prism 提供了一些指导,旨在帮助您更轻松地设计和构建丰富、灵活且易于维护的 Windows Presentation Foundation (WPF) 桌面应用程序、Silverlight Rich Internet Applications (RIA) 以及 Windows Phone 7 应用程序。 通过使用可体现关注点分离和松散耦合等重要体系结构设计原则的设计模式,Prism 可帮助您设计和构建使用松散耦合组件的应用程序,这些组件不仅可以独立演化,而且可以轻松地无缝集成到整个应用程序中。 这些类型的应用程序称为复合应用程序。”

MVVM 的普及和成功使 MVPVM 黯然失色,即便 MVPVM 是从 MVVM 演变而来(本文将会阐明这一点)。 MVPVM 提供了 MVVM 的所有强大功能,同时引入了 Model-View-Presenter (MVP) 模式的可伸缩性和可扩展性。 如果您了解到 MVVM 是从用于 WPF 的 MVP 演变而来,您就会发现本文的启发意义所在。

在完全认识 MVPVM 的能力和优点之前,我们必须了解这种模式是如何演变的,我们必须了解这一历史过程。 Model-View-Controller (MVC) 模式是了解这一过程的重要部分,所以我首先将介绍 MVC,也许您从未见过这样的讲解方式。

MVC 模式

请注意,针对 MVC 的这一讨论仅限于桌面应用程序范畴;而 Web 应用程序则是另外一回事,不在本文讨论范围之内。

在 Martin Fowler 撰写的文章“GUI Architectures”(bit.ly/11OH7Y) 中,他指出与 MVC 有关的以下内容: “在不同地方阅读 MVC 相关文章的不同人员对 MVC 提出了不同的观点,并且将这些观点描述为‘MVC’。如果这并没有造成足够的混淆,那么您会领教通过一连串的耳语流传演变而来的 MVC 误解。“Whatsa Controller Anyway”一文(网址为 bit.ly/7ajVeS)很好地总结了他的观点,他指出: “计算机科学家总是具有一种令人讨厌的使术语复杂化的倾向。 也就是说,他们往往会将许多含义(有时自相矛盾)赋予同一个词。”

由于开发人员缺乏作为出色架构师在架构环境方面的共同核心经验,这一事实使 Smalltalk MVC 变得更加复杂。 同样,由于我们从不需要自己去使用控制器,是操作系统总是为我们处理控制器的功能,因此我们大多数人甚至根本不了解什么是控制器,这一事实也使术语复杂化和“耳语流传”的现象越来越严重。 凭借几项事实来提供我们所缺少的共同核心经验,您会发现 MVC 实际上很容易理解,MVC 中的“C”与 MVP 中的“P”毫无共同之处。

控制器确实有具体的出处(这很值得了解,但没必要在当前技术背景下将现代方法与 MVC 搭上关系)。 MVC 的创立人 Trygve Reenskaug 于 1979 在其文章中撰写了“Models-Views-Controllers”的有关内容 (bit.ly/2cdqiu)。 这篇文章开始对控制器用途提供了一些见解。 Reenskaug 写道:

“控制器是用户与系统之间的纽带。 它通过安排相关视图在屏幕上的适当位置呈现,向用户提供输入; 并且通过向用户呈现菜单或显示命令和数据的其他方式,提供用于用户输出的方法。 控制器可接收此类用户输出,将其转换为适当的消息并将这些消息传递给一个或多个视图。

控制器永远不会作为视图的补充,例如它绝对不会通过在节点视图之间绘制箭头将视图连接起来。

相反,视图绝对不会识别用户输入,例如鼠标操作和键击等。 在控制器中应当总是能够编写一个向视图发送消息的方法,而这些视图会精确地再现任何用户命令序列。”

图 1 中阐释了这一概念。

MVC 面向对象编程 (OOP) 上下文中的“消息”是一种执行方法的途径。 之所以被描述成“消息”的原因是,当时虚拟调用是一个新概念,并且是一种与静态函数调用进行区分的方式。 在 Trevor Hopkins 和 Bernard Horan 合著的《Smalltalk, an Introduction to Application Development Using VisualWorks》(Prentice Hall,1995 年)一书的章节 1.2 中,作者指出“… 如果接收对象识别它所发送的消息,则将执行其某一内部操作(或方法)。 反过来,这可能又会导致执行某些计算(通过对一个或多个对象的内部变量进行操作)。”(注: 在 Prism 中通过其 EventAggregator 提供了这一“消息发送”OOP 概念。)

这本书在第 29 章“Introduction to Models, Views and Controllers”中明确地概述了 Smalltalk MVC 的责任,使我们了解到控制器源于 Controller 类。 其中指出“该类的实例引用代表鼠标和键盘的传感器,以便它可以处理输入。”后面还指出从控制器启动的两个不同操作是与模型通信(请参见图 1)以及与视图通信而不影响到模型。

Controller Receives Input and Sends Message (Method to Execute); Views Can’t Catch User Input
图 1:控制器接收输入和发送消息(执行方法);视图无法捕获用户输入

在 Smalltalk MVC 中,“每个”视图都将具有控制器,并且在任何给定时间只能有一个控制器处于活动状态。 在原始 Smalltalk MVC 中,对 UI 进行轮询;在需要进行控制时,顶级 ControlManager 类会向活动视图的每个控制器发出请求。 只有包含光标的视图才能接受控制。 如果视图是只读的,则可以使用 NoController 类,该类设计为拒绝控制。 包含子视图的视图负责轮询其子视图的控制器。 在控制器接受控制后,它将查询键盘或鼠标的结果,并且在适当的时候将鼠标移动、按钮单击等消息发送到视图或模型。 用 MVVM 术语来说,这相当于在视图的代码隐藏中或通过 ViewModel 命令订阅控件的事件。 当用户与控件交互时,将调用适用的方法。

现在,您可以开始大致了解控制器在 MVC 上下文以及不自动处理事件的环境中的用途的背景知识。 与 Smalltalk MVC 开发人员不同的是,WPF 开发人员无需查询键盘缓冲区和程序包并引发事件。 您只需订阅它们,适用的方法就会通过 Microsoft Event Pattern“自动”接收事件。 了解这一点之后,下面的概念也许具有比较清楚的不同含义。

在 Steve Burbeck 撰写的文章“Applications Programming in Smalltalk-80: How to Use Model-View-Controller (MVC)”(bit.ly/3ZfFCX) 中,他指出:

“控制器解释用户的鼠标和键盘输入,向模型和/或视图发出命令,使其根据需要进行更改。 最后,模型会管理应用程序域的行为和数据,针对与其状态相关的信息的请求(通常来自视图)做出响应,并响应更改状态的指令(通常来自控制器)。”

有关这一概念的说明,请参见图 2

Smalltalk Model-View-Controller
图 2:Smalltalk Model-View-Controller

最后要提的一点可能是一个含糊不清的主题,该主题在 Andy Bower 与 Blair McGlashan 合著的《Twisting the Triad, the Evolution of the Dolphin Smalltalk MVP Application Framework》文章中阐明,网址为 bit.ly/o8tDTQ(可下载的 PDF)。 这是我阅读过的对这一主题阐述比较全面的一篇文章。 作者指出了有关控制器的以下内容: “毫无疑问,视图负责显示域数据,而控制器负责处理将最终对这些数据执行操作的原始用户手势。”

我之所以提及这最后一点是为了借机提供另一个定义。 若要完全弄懂这篇文章(以及有关 MVC 的其他文章),您必须了解“手势”的含义(请参见图 1)。 在我进行研究时,碰巧发现“An Introduction to GUI Programming with UIL and Motif”(bit.ly/pkzmgc) 这篇文章,其中指出:

“这些步骤的实质是在用户请求某一操作之前,Motif 程序不执行任何操作。 将通过选择菜单项、单击按钮或其他窗口或者按某个键来请求执行操作。 这些请求方式统称为用户手势。 每个手势都会触发一个用于执行某些任务的应用程序功能。”

“Twisting the Triad”的合著者 Andy Bower 与我分享了他对“手势”的观点:

“我认为‘手势’是使某些东西更有意义的一个或多个用户事件的组合。

例如,TextController 可能会吸收向下键和向上键事件并将其转换为各自的“按键”手势。 同样,SelectionInListController(附加到列表框的控制器)可能会吸收列表中的多个鼠标按钮按下、鼠标按钮跟踪和鼠标按钮弹起事件,并将其视为单个列表“选择”手势。

看到这里,我们就明白事件驱动的现代操作系统已经为我们完成这一手势的大多数处理工作。”

在总结控制器逻辑时,您会发现控制器功能在所提到的各种 MVC 形式之间是几乎一致的。 由于 Microsoft 控件(小组件)处理“用户的鼠标和键盘输入”,因此您只需订阅事件并指向需要执行的方法即可,智能控件中的“控制器”会为您执行方法(在操作系统级别处理)。 同样,您也无需去使用控制器,这一点在“Twisting the Triad”一文中已明确指出。 如果没有控制器,就剩下用于 Windows 应用程序的 Model-View 模式。 在研究 MVC 及其应用程序和表示模型时,您必须记住这一点,因为没有控制器,这个三要素模式也就不复存在(图 3)。

Without the Controller, It Has Been Model-View (not Model-View-Controller)
图 3:在没有控制器的情况,该模式变成 Model-View(而不是 Model-View-Controller)

值得一提的是,不仅仅是控制器造成了 MVC 概念的模糊不清。 使用 Smalltalk MVC 时,业务/域逻辑位于模型中;而使用 VisualWorks MVC 时,ApplicationModel 包含向用户呈现一个或多个业务/域对象所需的“应用程序”逻辑(请参见图 4)。 “Twisting the Triad”一文对此进行更详细的说明。 如果您认识到应用程序模型和表示模型具有相同的功能,唯一不同之处是表示模型“不能”访问视图(为避免术语复杂化,Martin Fowler 已进行了明确区分),那么 WPF 开发人员可以快速地掌握表示模型,因为它实质上就是 MVVM。 MVVM 的创立人 John Gossman 在其“PresentationModel and WPF”博客文章(网址为 bit.ly/pFs6cV)中解释了这一点。 与 Martin Fowler 一样,他也十分注重避免术语复杂化。 这实际上使我们可以清楚地了解 MVVM 是什么;它是“PresentationModel 模式的 WPF 特定版本”。

VisualWorks Model-View-Controller
图 4:VisualWorks Model-View-Controller

使 MVC 真相大白后,将对您了解模型和视图的真正角色十分有帮助。 模型和视图实质上只是业务/域逻辑的两个位置。 阅读“Twisting the Triad”后,您会发现应用程序模型的责任已转移到表示器,您不能简单地将控制器替代为表示器,这没有意义,因为这两者的含义不同。

MVP 模式

花心思研究 MVP 已超出本文的范围;幸运的是,有许多文章(如“Twisting the Triad”和“Potel Paper”)讲述了 MVP,可从 bit.ly/dF4gNn (PDF) 下载这些文章。

不过,我将指出的是,“Twisting the Triad”一文阐明了一个我间接指出的重要观点,这个观点导致 MVC 演变为 MVP:

“MVC 的另一个棘手功能是,至少就 Dolphin 而言,控制器的理念无法完全适合 Windows 环境。 Microsoft Windows 与大多数现代图形操作系统一样,也提供了一组可用于构建用户界面的本机小组件。 这些‘窗口’已包含大多数控制器功能,而这些功能作为基础操作系统控件的一部分发挥作用。”

我还希望着重介绍一下 Martin Fowler 在其“GUI Architectures”文章中的陈述,他指出:“这种需要直接操作小组件的方式已被许多人视为有些令人厌恶的解决方法,从而促使 Model-View-Presenter 方法的发展。”了解这一点十分重要,因为 MVVM 使表示模型思维模式已在许多 WPF 开发人员的头脑中根生蒂固;他们认为直接更新视图是“错误的编程”。 对于 MVVM 和表示模型确实是这样,因为在引用视图后,无法再将 ViewModel 重新用于其他视图(这一规则的主要原因)。 这一限制因素是促使 Smalltalk MVC 演变为 MVP 模式的原因之一。 利用 MVPVM,开发人员可以直接访问视图和 ViewModel(这是紧密耦合的),这使视图和 ViewModel 可以完全分离(请参见图 5)。 视图可以重用不同的 ViewModel,而 ViewModel 也可以由不同的视图重用(在下文讨论 MVPVM 模式时您将会了解到这一点);这是 MVPVM 的许多重大优点之一。

Model-View-Presenter, Supervising Controller
图 5:Model-View-Presenter,Supervising Controller

为了进一步予以阐明,Andy Bower 与我分享了有关这一主题的以下信息:

“也许值得指出的是,所有这些模式的宗旨是通过可插接性进行重用。 尽量保持松散耦合并且尽量减小接口,使组件在重新组合方面能够最大限度地得到重用。

不过,请注意模型实际上有两个接口,它们必须与所有关联的视图和表示器兼容。 第一个是标准函数调用接口,用于获取/设置值和执行命令。 第二个是事件接口,它必须与视图所需的接口完全相同(观察器模式)。 这一接口组合可称为“协议”。

例如,在 Dolphin MVP 中,列表小组件(三层架构)需要具有兼容协议的三个组件。 ListModel、ListView 和 ListPresenter。”

在了解为何不希望直接更新视图的原因后,您会发现这一原因促成了一个新设计模式的产生,该模式使您能够有效地重用对象,消除无法更新视图的限制,因此您应该选择抓住这一演变提供的新机会。 您生活在一个不断发展变化的世界中,代码也是一样,您必须改变思维模式,“避免让历史重演”。

MVPVM 模式

我是在 Prism 生命周期初期学习到这种模式的,具体时间是在 CompositeWP 的第 7 期。 在揭开 Prism 神秘面纱的过程中,我认识了 MVP 模式,这种模式帮助我掌握了 Prism 的初步知识。 我这样快速学习(因为我需要向新开发人员授课)的目的就是为了研究表示器,而您将需要从了解代码和应用程序开始学习。 可以在以后学习如何执行代码,这不一定会妨碍关键时间表;可以根据难度自行掌握学习进度。

MVPVM 组件的以下定义适当地包含了“Twisting the Triad”的一些摘录;我发现这些信息很有启示,既全面又准确,因此为了尽可能地向您传达最适合的信息,我只引用了这篇文章中的定义。 虽然这些定义是在 2000 年 3 月撰写“Twisting the Triad”时针对 MVP 提出的,但在今天它们仍然适用于 MVPVM(请参见图 6)。

Model-View-Presenter-ViewModel
图 6:Model-View-Presenter-ViewModel

(注意: 本文的讨论背景是 Prism,但 MVPVM 将在不使用 Prism 的情况下工作;各种组件只是紧密耦合到其实现中,这与通过 Unity 或托管可扩展性框架 [MEF] 依赖关系注入容器进行解析(即松散耦合)相反。)

MVPVM: 模型

“这是用户界面将要处理的数据。 它通常是一个域对象,此类对象应该特意无法识别用户界面。”- 摘自“Twisting the Triad”

在处理依赖关系注入的关注点以及使用模型在业务逻辑层 (BLL) 和数据访问层 (DAL) 中创建、读取、更新、删除和列出 (CRUDL) 持久性数据时,需要将模型与 ViewModel 分离。 在 MVPVM 中,只有 DAL 有权访问持久性模型对象。

MVPVM: 视图

“视图在 MVP 中的行为与在 MVC 中的行为大体相同。 视图的责任是显示模型的内容。 在每次修改其数据时,模型都应该触发适当的更改通知,然后这些通知会使视图按照标准观察器模式“挂起”模型。 MVC 也采用同样的方式,这使多个视图可以连接到单个模型。”- 摘自“Twisting the Triad”

(注意: 我运用了前面的引述来强调 MVP 并不是一种新模式,在今天 MVP 与最初从 MVC 演变时同样有效。 不过,该引述指的是“模型”,而 MVPVM 使用的是“ViewModel”。)

利用 MVPVM,永远不需要在代码隐藏中编写代码。 表示器可以访问视图并可以根据需要订阅事件、操作控件和操作 UI。 在开发本文随附的多重目标应用程序时,这一功能被证明十分有用。 用户列表框不关心选定内容更改事件的数据绑定,因此我必须开发适用于所有这三种平台的方法(因为它们共享相同的代码)。 在激活视图时,我将调用 WireUpUserListBox(请参见图 7,第 174 行)并使用视图获取对 lstUserList 控件(驻留在 DataTemplate 中)的引用。 在找到该控件后,将其绑定到 SelectionChanged 事件并更新 ViewModel SelectedUser 属性。 这适用于桌面、Silverlight 和 Windows Phone。

Benefits of Being Able to Access the View Directly
图 7:能够直接访问视图的优点

视图无法识别 ViewModel,因此它们不是紧密耦合的。 只要 ViewModel 支持视图绑定的所有属性,它就可以轻松地使用视图。 表示器负责通过将视图的 DataContext 设置为适当的 ViewModel,将视图绑定到其 ViewModel。

MVPVM: 表示器

“虽然视图的责任是显示模型数据,但由表示器控制用户界面操作和更改模型的方式。 这是应用程序行为的核心所在。 在许多方面,MVP 表示器等同于 MVC 中的应用程序模型;用于处理用户界面工作方式的大多数代码已内置到表示器类中。 二者的主要区别在于表示器直接链接到其关联视图,这使它们可以在为特定模型提供用户界面的角色中紧密合作。”- 摘自“Twisting the Triad”

表示器在需要从中检索域对象(数据)的 BLL 界面上具有依赖关系,如图 8 的左窗格中所示。 它将按模块中的配置使用已解析的实例(请参见图 8,右下方的窗格)或使用引导程序访问数据并填充 ViewModel。

Security Module Code
图 8:安全模块代码

通常,只有表示器会紧密耦合在 MVPVM 组件中。 它将紧密耦合到视图、ViewModel 和 BLL 接口。 表示器不适合重用;它们处理特定的关注点以及这些关注点的业务逻辑/规则。 在可以跨企业应用程序重用表示器的方案中,模块可能更适合于任务。也就是说,可以创建一个可由所有企业应用程序重用的登录模块(项目)。 如果针对某个接口编写代码,则可以使用 MEF 或 Unity 等依赖关系注入技术轻松地重用模块。

MVPVM: ViewModel

“在 MVP 中,模型只是一个域对象,根本无意使用(或链接到)用户界面。”- 摘自“Twisting the Triad”

这可以避免 ViewModel 紧密耦合到单个视图,使它可以被许多视图重用。 同样,ViewModel 也没有应用程序业务逻辑,因此可以在企业应用程序之间轻松地共享 ViewModel。 这促进了重用和应用程序集成。 例如,看一下图 8,右上方窗格中的第 28 行。 这个 SecurityCommandViewModel 位于 Gwn.Library.MvpVm.xxxx 项目中(其中 xxxx 是 Silverlight、桌面或 Windows Phone)。 由于大多数应用程序都将需要用户 ViewModel,因此这是一个可重用的组件。 我必须注意,不让该组件被演示应用程序的特定业务逻辑弄乱。 这对于 MVPVM 来说不是问题,因为业务逻辑将由表示器处理,而不是在 ViewModel 中处理(请参见图 6)。

(注意: 当 ViewModel 属性由表示器填充时,它们将引发 NotifyPropertyChanged 事件,这将提醒视图存在新数据。在收到通知后,视图负责根据 ViewModel 数据对自身进行更新。)

业务逻辑层

BLL 无法识别持久性模型数据。 它们完全使用域对象和接口。 通常,您将在 BLL 中使用依赖关系注入来解析 DAL 接口,因此可以稍后换出 BLL 而不会影响任何下游代码。 您在图 9 中的第 34 行可以看到,我在演示应用程序中对 IBusinessLogicLayer 使用了 MockBll 实现。 随后,我只需一行代码就可以轻松地将它替换为接口的产品实现,因为我已针对该接口进行了开发工作。

Registering BLL, DAL and Commands
图 9:注册 BLL、DAL 和命令

业务逻辑并不仅局限于 BLL。 在图 9 中,我注册了指定的类型(用于 IPresenterCommand),以便可以将依赖关系注入用作工厂。 当用户单击按钮时(图 10 中的第 29–33 行),将由基类解析(实例化)命令参数,然后执行适用的命令。 例如,LoginCommand(图 8,右上方窗格)是一个将激活 UserManagerView 的命令。 将此连接起来所需的全部内容就是 XAML 中的 Button 命令以及 SecurityModule 中的一个条目(请参见图 8,右下方窗格的第 32 行)。

DataTemplate for the UserCommandViewModel
图 10:用于 UserCommandViewModel 的 DataTemplate

数据访问层

持久性域数据可存储在 SQL 数据库、XML 文件或文本文件中,也可从 REST 服务检索。 使用 MVPVM 时,只有 DAL 保存检索数据所需的特定信息。 DAL 将仅向 BLL 返回域对象。 这使 BLL 无需识别连接字符串、文件句柄、REST 服务等等。 这提高了可伸缩性和可扩展性;您可以轻松地将 DAL 从 XML 文件切换到云服务,而不影响 DAL 和应用程序配置文件外部的任何现有代码。 只要新的 DAL 单元测试适用于 CRUDL 进程,就可以将应用程序配置为使用新 DAL 而不影响当前应用程序及其使用。

见证历史,借鉴经验

衷心感谢模式和实施方案团队,正是由于他们的工作,我才能顺利地利用前沿技术和模式(如 MVPVM)使我的客户获得成功。 我一直努力跟上这个团队的步伐、技术和模式,当更新的技术和模式出现时,我可以汲取以往的经验,从而帮助我在新领域中轻松地找到适合自己的方法。

MVP 是始终如一的模式,自从早期使用模式和实施方案项目进行体系结构研究以来,我就一直见证这一点。 要完全认识 MVP,您必须了解模式的历史,特别是 MVC 的组件。 由于涉及许多因素,这不是很容易办到。

通过了解模式的过去以及演变原因,我们可以更快地领会更新的模式的需求及其功能。 即便面对 Prism 以及其复杂性和奇妙之处,您也会通过了解这一历史并展望对更新的模式的需求,顺利地完成比较困难的学习过程。 自这一模式在 Prism 早期出现以来,在我所参与过的所有成功项目中,使用 MVPVM 时未遇到过什么问题、障碍或困难(这些似乎已通过演变而消除),这使开发人员可以加快构建可伸缩、可扩展和稳定应用程序的速度。

Bill Kratochvil是一位独立签约人,同时也是一个由开发人员组成的精英团队的首席技术专家和架构师,该团队正在从事医疗行业的一家领先公司的一个机密项目。 他自己的公司 Global Webnet LLC 位于美国德克萨斯州阿马里洛。

衷心感谢以下技术专家对本文的审阅: Andy BowerChristina HeltonSteve Sanderson