在上一篇《ASP.NET MVC框架新手入门学习教程之MVC简介》中我们讲解了关于MVC的介绍,今天我们介绍MVC的变体。
通过采用MVC 模式,我们可以将可视化UI 元素的呈现、UI 处理逻辑和业务逻辑分别定义在View 、Controller 和Model 中,但是对于三者之间的交互, MVC 并没有进行严格的限制。最为典型的就是允许View 和Model 绕开Controller 进行直接交互, View 可以通过调用Model 获取需要呈现给用户的数据, Model 也可以直接通知View 让其感知到状态的变化。当我们将MVC 应用于具体的项目开发中,不论是基于GUI 的桌面应用还是基于WebUI 的Web 应用,如果不对Model 、View 和Controller 之间的交互进行更为严格的限制,我们编写的程序可能比自治视图更加难以维护。
今天我们将MVC 视为一种模式CP硝ern) ,但是作为MVC 最初提出者的Trygve M. H.Reenskau 却将MVC 视为一种范例。模式和范例的区别在于前者可以直接应用到具体的应用上,而后者则仅仅提供一些基本的指导方针。在我看来MVC 是一个很宽泛的概念,任何基于Model 、View 和Controller 对UI 应用进行分解的设计都可以成为MVC 。当我们采用MVC 的思想来设计UI 应用的时候,应该根据开发框架(比如Windows Forms 、WPF 和Web Forms) 的特点对Model 、View 和Con位oller 的界限以及相互之间的交互设置一个更为严格的规则。
在软件设计的发展历程中出现了一些MVC 的变体C V:缸ation) ,它们遵循定义在MVC中的基本原则,我们现在来简单地讨论一些常用的MVC 变体。
MVP 是一种广泛使用的UI 架构模式,适用于基于事件驱动的应用框架,比如ASP.NET Web Forms 和Windows Forms 应用。MVP 中的M 和V 分别对应于MVC 的Model 和View ,而P (Presenter )则自然代替了MVC 中的Controller 。但是MVP 并非仅仅体现在从Controller到Presenter 的转换,更多地体现在Model 、View 和Presenter 之间的交互上。
MVC 模式中元素之间"混乱"的交互主要体现在允许View 和Model 绕开Controller 进行单独"交流",这在MVP 模式中得到了彻底解决。如图所示,能够与Model 直接进行交互的仅限于Presenter , View 只能通过Presenter 间接地调用Modelo Model 的独立性在这里得到了真正的体现,它不仅仅与可视化元素的呈现(View) 无关,与UI 处理逻辑(Presenter)也无关。使用MVP 的应用是用户驱动的而非Model 驱动的,所以Model 不需要主动通知View 以提醒状态发生了改变。
MVP 不仅仅避免了View 和Model 之间的稿合,更进一步地降低了Presenter 对View 的依赖。如图1-2 所示, Presenter 依赖的是一个抽象化的View , ep View 实现的接口IView ,这带来的最直接的好处就是使定义在Presenter 中的UI 处理逻辑变得易于测试。由于Presenter对View 的依赖行为定义在接口IView 中,我们只需要Mock 一个实现了该接口的View 就能对Presenter 进行测试。
构成MVP 三要素之间的交互体现在两个方面,即ViewlPresenter 和PresenterIModel 。Presenter 和Model 之间的交互很清晰,仅仅体现在Presenter 对Model 的单向调用。而View和Presenter 之间该采用怎样的交互方式是整个MVP 的核心, MVP 针对关注点分离的初衷能否体现在具体的应用中很大程度上取决于两者之间的交互方式是否正确。按照View 和Presenter 之间的交互方式以及View 本身的职责范围, Martin Folwer 将MVP 可分为PV(Passive View) 和SC (Supervising Controller) 两种模式。
解决View 难以测试的最好的办法就是让它无需测试,如果View 不需要测试,其先决条件就是让它尽可能不涉及到UI 处理逻辑,这就是PV 模式目的所在。顾名思义, PV (PassiveView) 是一个被动的View ,包含其中的针对UI 元素(比如控件)的操作不是由View 自身主动来控制,而被动地交给Presenter 来操控。
如果我们纯粹地采用PV 模式来设计View ,意味着我们需要将View 中的U 元素通过属性的形式暴露出来。具体来说,当我们在为View 定义接口的时候,需要定义基于UI 元素的属性使Presenter 可以对View 进行细粒度操作,但这并不意味着我们直接将View 上的控件暴露出来。举个简单的例子,假设我们开发的HR 系统中具有如图1-3 所示的一个Web 页面,我们通过它可以获取某个部门的员工列表。
现在通过ASP.NET Web Forms 应用来设计这个页面,我们来讨论一下如果采用PV 模式,View 的接口该如何定义。对于Presenter 来说, View 供它操作的控件有两个,一个是包含所有部门列表的DropDownList,另一个则是显示员工列表的GridView 。在页面加载的时候,Presenter 将部门列表绑定在DropDownList 上,与此同时包含所有员工的列表被绑定到GridView。当用户选择某个部门并点击"查询"按钮后, View 将包含筛选部门在内的查询请求转发给Presenter,后者筛选出相应的员工列表之后将其绑定到GridView 。
如果我们为该View 定义一个接口IEmployeeSearchView ,我们不能按照所示的代码将上述这两个控件直接以属性的形式暴露出来。针对具体控件类型的数据绑定属于View 的内部细节(比如说针对部门列表的显示,我们可以选择DropDownList 也可以选择ListBox) ,不能体现在表示用于抽象View 的接口中。另外,理想情况下定义在Presenter 中的U 处理逻辑应该是与具体的技术平台无关的,如果在接口中涉及控件类型,这无疑将Presenter 也与具体的技术平台绑定在了一起。
正确的接口和实现该接口的View (一个Web 页面)应该采用如下的定义方式。Presenter通过对属性Departments 和Employees 赋值进而实现对相应DropDownList 和GridView 的数据绑定,通过属性SelectedDepartment 得到用户选择的筛选部门。为了尽可能让接口只暴露必需的信息,我们特意将对属性的读/写作了控制。看下面的定义:
ErnployeeSearchView继承Page和实现IErnployeeSearchView
PV 模式将所有的UI 处理逻辑全部定义在Presenter 上,意味着所有的UI 处理逻辑都可以被测试,所以从可测试性的角度来这是一种不错的选择,但是它要求将View 中可供操作的UI 元素定义在对应的接口中,对于一些复杂的富客户端(Rich Client) View 来说,接口成员将会变得很多,这无疑会提升编程所需的代码量。从另一方面来看,由于Presenter 需要在控件级别对View 进行细粒度的控制,这无疑会提供Presenter 本身的复杂度,往往会使原本简单的逻辑复杂化,在这种情况下我们往往采用SC 模式。
在SC 模式下,为了降低Presenter 的复杂度,我们将诸如数据绑定和格式化这样简单的U 处理逻辑转移到View 中,这些处理逻辑会体现在View 实现的接口中。尽管View 从Presenter 中接管了部分UI 处理逻辑,但是Presenter 依然是整个三角关系的驱动者, View 被动的地位依然没有改变。对于用户作用在View 上的交互操作, View 本身并不进行响应,而是直接将交互请求转发给Presenter ,后者在独立完成相应的处理流程(可能涉及针对Model的调用)之后会驱动View 或者创建新的View 作为对用户交互操作的响应。
View 和Presenter 之间的交互是整个MVP 的核心,能否正确地应用MVP 模式来架构我们的应用主要取决于能否正确地处理View 和Presenter 两者之间的关系。在由Model 、View和Presenter 组成的三角关系中,核心不是View 而是Presenter , Presenter 不是View 调用Model的中介,而是最终决定如何响应用户交互行为的决策者。
打个比方, View 是Presenter 委派到前端的客户代理,而作为客户的自然就是最终的用户。对于以鼠标/键盘操作体现的交互请求应该如何处理,作为代理的View 并没有决策权,所以它会将请求汇报给委托人Presenter 0 View 向Presenter 发送用户交互请求应该采用这样的口吻: "我现在将用户交互请求发送给你,你看着办,需要我的时候我会协助你",而不应该是这样: "我现在处理用户交互请求了,我知道该怎么办,但是我需要你的支持,因为实现业务逻辑的Model 只信任你"。
对于Presenter 处理用户交互请求的流程,如果中间环节需要涉及到Model ,它会直接发起对Model 的调用。如果需要View 的参与(比如需要将Model 最新的状态反应在View 上),Presenter 会驱动View 完成相应的工作。
对于绑定到View 上的数据,不应该是View 从Presenter _ t" 拉"回来的,应该是Presenter主动"推"给View 的。从消息流(或者消息交换模式)的角度来讲,不论是View 向Presenter完成针对用户交互请求的通知,还是Presenter 在进行交互请求处理过程中驱动View 完成相应的UI 操作,都是单向COne-Way) 的。反应在应用编程接口的定义上就意味着不论是定义在Presenter 中被View 调用的方法,还是定义在IView 接口中被Presenter 调用的方法最好都没有返回值。如果不采用方法调用的形式,我们也可以通过事件注册的方式实现View 和Presenter 的交互,事件机制体现的消息流无疑是单向的。
View 本身仅仅实现单纯的、独立的UI 处理逻辑,它处理的数据应该是Presenter 实时推送给它的,所以View 尽可能不维护数据状态。定义在IView 的接口最好只包含方法,而避免属性的定义, Present巳r 所需的关于View 的状态应该在接收到View 发送的用户交互请求的时候一次得到,而不需要通过View 的属性去获取。
好了关于MVC的介绍也差多了,详细你对MVC的演变等历史或者概念已经完全明白了,在下面的课程中我们将讲解关于MVC的处理流程问题,这对于整个程序的整体把握很有必要的。