MVP
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 模式中得到了彻底解决。如图1-2 所示,能够与Model 直接进行
交互的仅限于Presenter,View 只能通过Presenter 间接地调用Model。Model 的独立性在这
里得到了真正的体现,它不仅仅与可视化元素的呈现(View)无关, 与UI 处理逻辑(Presenter)
也无关。使用MVP 的应用是用户驱动的而非Model 驱动的,所以Model 不需要主动通知
View 以提醒状态发生了改变。
图 1-2 Model-View-Presenter 之间的交互
MVP 不仅仅避免了View 和Model 之间的耦合,更进一步地降低了Presenter 对View 的
依赖。如图1-2 所示,Presenter 依赖的是一个抽象化的View,即View 实现的接口IView,
这带来的最直接的好处就是使定义在Presenter 中的UI处理逻辑变得易于测试。由于Presenter
对View 的依赖行为定义在接口IView 中,我们只需要Mock 一个实现了该接口的View 就能
对Presenter 进行测试。
构成 MVP 三要素之间的交互体现在两个方面,即View/Presenter 和Presenter/Model。
Presenter 和Model 之间的交互很清晰,仅仅体现在Presenter 对Model 的单向调用。而View
和Presenter 之间该采用怎样的交互方式是整个MVP 的核心,MVP 针对关注点分离的初衷
能否体现在具体的应用中很大程度上取决于两者之间的交互方式是否正确。按照View 和
Presenter 之间的交互方式以及View 本身的职责范围,Martin Folwer 将MVP 可分为PV
(Passive View)和SC(Supervising Controller)两种模式。
PV 与SC
解决 View 难以测试的最好的办法就是让它无需测试,如果View 不需要测试,其先决
第1 章 ASP.NET + MVC
ASP.NET MVC 4 框架揭秘
6
条件就是让它尽可能不涉及到UI 处理逻辑,这就是PV 模式目的所在。顾名思义,PV(Passive
View)是一个被动的View,包含其中的针对UI 元素(比如控件)的操作不是由View 自身
主动来控制,而被动地交给Presenter 来操控。
如果我们纯粹地采用PV 模式来设计View,意味着我们需要将View 中的UI 元素通过
属性的形式暴露出来。具体来说,当我们在为View 定义接口的时候,需要定义基于UI 元素
的属性使Presenter 可以对View 进行细粒度操作,但这并不意味着我们直接将View 上的控
件暴露出来。举个简单的例子,假设我们开发的HR 系统中具有如图1-3 所示的一个Web 页
面,我们通过它可以获取某个部门的员工列表。
图 1-3 员工查询页面
现在通过 ASP.NET Web Forms 应用来设计这个页面,我们来讨论一下如果采用PV 模式,
View 的接口该如何定义。对于Presenter 来说,View 供它操作的控件有两个,一个是包含所
有部门列表的DropDownList,另一个则是显示员工列表的GridView。在页面加载的时候,
Presenter 将部门列表绑定在DropDownList 上,与此同时包含所有员工的列表被绑定到
GridView。当用户选择某个部门并点击“查询”按钮后,View 将包含筛选部门在内的查询
请求转发给Presenter,后者筛选出相应的员工列表之后将其绑定到GridView。
如果我们为该 View 定义一个接口IEmployeeSearchView,我们不能按照所示的代码将上
述这两个控件直接以属性的形式暴露出来。针对具体控件类型的数据绑定属于View 的内部
细节(比如说针对部门列表的显示,我们可以选择DropDownList 也可以选择ListBox),不
能体现在表示用于抽象View 的接口中。另外,理想情况下定义在Presenter 中的UI 处理逻
辑应该是与具体的技术平台无关的,如果在接口中涉及控件类型,这无疑将Presenter 也与
具体的技术平台绑定在了一起。
public interface IEmployeeSearchView
{
DropDownList Departments { get;}
GridView Employees { get; }
}
正确的接口和实现该接口的View(一个Web 页面)应该采用如下的定义方式。Presenter
通过对属性Departments 和Employees 赋值进而实现对相应DropDownList 和GridView 的数
1.2 MVC 的变体
ASP.NET MVC 4 框架揭秘
7
据绑定,通过属性 SelectedDepartment 得到用户选择的筛选部门。为了尽可能让接口只暴露
必需的信息,我们特意将对属性的读/写作了控制。
public interface IEmployeeSearchView
{
IEnumerable<string> Departments { set; }
string SelectedDepartment { get; }
IEnumerable<Employee> Employees { set; }
}
public partial class EmployeeSearchView: Page, IEmployeeSearchView
{
//其他成员
public IEnumerable<string> Departments
{
set
{
this.DropDownListDepartments.DataSource = value;
this.DropDownListDepartments.DataBind();
}
}
public string SelectedDepartment
{
get { return this.DropDownListDepartments.SelectedValue;}
}
public IEnumerable<Employee> Employees
{
set
{
this.GridViewEmployees.DataSource = value;
this.GridViewEmployees.DataBind();
}
}
}
PV 模式将所有的UI 处理逻辑全部定义在Presenter 上,意味着所有的UI 处理逻辑都可
以被测试,所以从可测试性的角度来这是一种不错的选择,但是它要求将View 中可供操作
的UI 元素定义在对应的接口中,对于一些复杂的富客户端(Rich Client)View 来说,接口
成员将会变得很多,这无疑会提升编程所需的代码量。从另一方面来看,由于Presenter 需
要在控件级别对View 进行细粒度的控制,这无疑会提供Presenter 本身的复杂度,往往会使
原本简单的逻辑复杂化,在这种情况下我们往往采用SC 模式。
在 SC 模式下,为了降低Presenter 的复杂度,我们将诸如数据绑定和格式化这样简单的
UI 处理逻辑转移到View 中,这些处理逻辑会体现在View 实现的接口中。尽管View 从
Presenter 中接管了部分UI 处理逻辑,但是Presenter 依然是整个三角关系的驱动者,View 被
动的地位依然没有改变。对于用户作用在View 上的交互操作,View 本身并不进行响应,而
是直接将交互请求转发给Presenter,后者在独立完成相应的处理流程(可能涉及针对Model
的调用)之后会驱动View 或者创建新的View 作为对用户交互操作的响应。
第1 章 ASP.NET + MVC
ASP.NET MVC 4 框架揭秘
8
View 和Presenter 交互的规则(针对SC 模式)
View 和Presenter 之间的交互是整个MVP 的核心,能否正确地应用MVP 模式来架构我
们的应用主要取决于能否正确地处理View 和Presenter 两者之间的关系。在由Model、View
和Presenter 组成的三角关系中,核心不是View 而是Presenter,Presenter 不是View 调用Model
的中介,而是最终决定如何响应用户交互行为的决策者。
打个比方,View 是Presenter 委派到前端的客户代理,而作为客户的自然就是最终的用
户。对于以鼠标/键盘操作体现的交互请求应该如何处理,作为代理的View 并没有决策权,
所以它会将请求汇报给委托人Presenter。View 向Presenter 发送用户交互请求应该采用这样
的口吻:“我现在将用户交互请求发送给你,你看着办,需要我的时候我会协助你”,而不应
该是这样:“我现在处理用户交互请求了,我知道该怎么办,但是我需要你的支持,因为实
现业务逻辑的Model 只信任你”。
对于 Presenter 处理用户交互请求的流程,如果中间环节需要涉及到Model,它会直接发
起对Model 的调用。如果需要View 的参与(比如需要将Model 最新的状态反应在View 上),
Presenter 会驱动View 完成相应的工作。
对于绑定到 View 上的数据,不应该是View 从Presenter 上“拉”回来的,应该是Presenter
主动“推”给View 的。从消息流(或者消息交换模式)的角度来讲,不论是View 向Presenter
完成针对用户交互请求的通知,还是Presenter 在进行交互请求处理过程中驱动View 完成相
应的UI 操作,都是单向(One-Way)的。反应在应用编程接口的定义上就意味着不论是定
义在Presenter 中被View 调用的方法,还是定义在IView 接口中被Presenter 调用的方法最好
都没有返回值。如果不采用方法调用的形式,我们也可以通过事件注册的方式实现View 和
Presenter 的交互,事件机制体现的消息流无疑是单向的。
View 本身仅仅实现单纯的、独立的UI 处理逻辑,它处理的数据应该是Presenter 实时推
送给它的,所以View 尽可能不维护数据状态。定义在IView 的接口最好只包含方法,而避
免属性的定义,Presenter 所需的关于View 的状态应该在接收到View 发送的用户交互请求的
时候一次得到,而不需要通过View 的属性去获取。
实例演示:SC 模式的应用(S101)
为了让读者对 MVP 模式,尤其是该模式下的View 和Presenter 之间的交互方式有一个
深刻的认识,我们现在来做一个简单的实例演示。本实例采用上面提及的关于员工查询的场
景,并且采用ASP.NET Web Forms 来建立这个简单的应用,最终呈现出来的效果如图1-3
所示。前面我们已经演示了采用PV 模式下的IView 应该如何定义,现在我们来看看SC 模
式下的IView 有何不同。
先来看看表示员工信息的数据类型如何定义。我们通过具有如下定义的数据类型
Employee 来表示一个员工。简单起见,我们仅仅定义了表示员工基本信息(ID、姓名、性
1.2 MVC 的变体
ASP.NET MVC 4 框架揭秘
9
别、出生日期和部门)的5 个属性。
public class Employee
{
public string Id { get; private set; }
public string Name { get; private set; }
public string Gender { get; private set; }
public DateTime BirthDate { get; private set; }
public string Department { get; private set; }
public Employee(string id, string name, string gender,
DateTime birthDate, string department)
{
this.Id = id;
this.Name = name;
this.Gender = gender;
this.BirthDate = birthDate;
this.Department = department;
}
}
作为包含应用状态和状态操作行为的Model 通过如下一个简单的EmployeeRepository 类
型来体现。如代码所示,表示所有员工列表的数据通过一个静态字段来维护,而GetEmployees
返回指定部门的员工列表,如果没有指定筛选部门或者指定的部门字符为空,则直接返回所
有的员工列表。
public class EmployeeRepository
{
private static IList<Employee> employees;
static EmployeeRepository()
{
employees = new List<Employee>();
employees.Add(new Employee("001", "张三", "男",
new DateTime(1981, 8, 24), "销售部"));
employees.Add(new Employee("002", "李四", "女",
new DateTime(1982, 7, 10), "人事部"));
employees.Add(new Employee("003", "王五", "男",
new DateTime(1981, 9, 21), "人事部"));
}
public IEnumerable<Employee> GetEmployees(string department = "")
{
if (string.IsNullOrEmpty(department))
{
return employees;
}
return employees.Where(e => e.Department == department).ToArray();
}
}
接下来我们来看作为View 接口的IEmployeeSearchView 的定义。如下面的代码片段所
示,该接口定义了BindEmployees 和BindDepartments 两个方法,分别用于绑定基于部门列
第1 章 ASP.NET + MVC
ASP.NET MVC 4 框架揭秘
10
表的 DropDownList 和基于员工列表的GridView。除此之外,IEmployeeSearchView 接口还
定义了一个事件DepartmentSelected,该事件会在用户选择了筛选部门后点击“查询”按钮
时触发。DepartmentSelected 事件参数类型为自定义的DepartmentSelectedEventArgs,属性
Department 表示用户选择的部门。
public interface IEmployeeSearchView
{
void BindEmployees(IEnumerable<Employee> employees);
void BindDepartments(IEnumerable<string> departments);
eventEventHandler<DepartmentSelectedEventArgs> DepartmentSelected;
}
public class DepartmentSelectedEventArgs : EventArgs
{
public string Department { get; private set; }
public DepartmentSelectedEventArgs(string department)
{
this.Department = department;
}
}
作为 MVP 三角关系核心的Presenter 通过EmployeeSearchPresenter 表示。如下面的代码
片段所示,表示View 的只读属性类型为IEmployeeSearchView 接口,而另一个只读属性
Repository 则表示作为Model 的EmployeeRepository 对象,两个属性均在构造函数中初始化。
public class EmployeeSearchPresenter
{
public IemployeeSearchView View { get; private set; }
public EmployeeRepository Repository { get; private set; }
public EmployeeSearchPresenter(IEmployeeSearchView view)
{
this.View = view;
this.Repository = new EmployeeRepository();
this.View.DepartmentSelected += OnDepartmentSelected;
}
public void Initialize()
{
IEnumerable<Employee> employees = this.Repository.GetEmployees();
this.View.BindEmployees(employees);
string[] departments =
new string[] { "销售部", "采购部", "人事部", "IT 部" };
this.View.BindDepartments(departments);
}
protected void OnDepartmentSelected(object sender,
DepartmentSelectedEventArgs args)
{
string department = args.Department;
var employees = this.Repository.GetEmployees(department);
this.View.BindEmployees(employees);
}
}
1.2 MVC 的变体
ASP.NET MVC 4 框架揭秘
11
在构造函数中我们注册了View 的DepartmentSelected 事件,作为事件处理器的
OnDepartmentSelected 方法通过调用Repository(即Model)得到了用户选择部门下的员工列
表,返回的员工列表通过调用View 的BindEmployees 方法实现了在View 上的数据绑定。在
Initialize 方法中,我们通过调用Repository 获取所有员工的列表,并通过View 的
BindEmployees 方法显示在界面上。作为筛选条件的部门列表通过调用View 的
BindDepartments 方法绑定在View 上。
最后我们来看看作为View 的Web 页面如何定义。如下所示的是作为页面主体部分的
HTML,核心部分是一个用于绑定筛选部门列表的DropDownList 和一个绑定员工列表的
GridView。
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>员工管理</title>
<link rel="stylesheet" href="Style.css" />
</head>
<body>
<form id="form1" runat="server">
<div id="page">
<div class="top">
选择查询部门:
<asp:DropDownList ID="DropDownListDepartments"
runat="server" />
<asp:Button ID="ButtonSearch" runat="server" Text="查询"
OnClick="ButtonSearch_Click" />
</div>
<asp:GridView ID="GridViewEmployees" runat="server"
AutoGenerateColumns="false" Width="100%">
<Columns>
<asp:BoundField DataField="Name" HeaderText="姓名" />
<asp:BoundField DataField="Gender" HeaderText="性别" />
<asp:BoundField DataField="BirthDate"
HeaderText="出生日期"
DataFormatString="{0:dd/MM/yyyy}" />
<asp:BoundField DataField="Department" HeaderText="部门"/>
</Columns>
</asp:GridView>
</div>
</form>
</body>
</html>
如下所示的是该 Web 页面的后台代码的定义,它实现了定义在IEmployeeSearchView 接
口的两个方法(BindEmployees 和BindDepartments)和一个事件(DepartmentSelected)。表
示Presenter 的同名只读属性在构造函数中被初始化。在页面加载的时候(Page_Load 方法)
Presenter 的Initialize 方法被调用,而在“查询”按钮被点击的时候(ButtonSearch_Click)事
件DepartmentSelected 被触发。
第1 章 ASP.NET + MVC
ASP.NET MVC 4 框架揭秘
12
public partial class Default : Page, IEmployeeSearchView
{
public EmployeeSearchPresenter Presenter { get; private set; }
public event EventHandler<DepartmentSelectedEventArgs> DepartmentSelected;
public Default()
{
this.Presenter = new EmployeeSearchPresenter(this);
}
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
this.Presenter.Initialize();
}
}
protected void ButtonSearch_Click(object sender, EventArgs e)
{
string department = this.DropDownListDepartments.SelectedValue;
DepartmentSelectedEventArgs eventArgs =
new DepartmentSelectedEventArgs(department);
if (null != DepartmentSelected)
{
DepartmentSelected(this, eventArgs);
}
}
public void BindEmployees(IEnumerable<Employee> employees)
{
this.GridViewEmployees.DataSource = employees;
this.GridViewEmployees.DataBind();
}
public void BindDepartments(IEnumerable<string> departments)
{
this.DropDownListDepartments.DataSource = departments;
this.DropDownListDepartments.DataBind();
}
}