[转载]MVC、MVP以及Model2[下篇] – Artech – 博客园.
5: }
正确的接口和实现该接口的View(一个Web页面)应该采用如下的定义方式。Presenter通过属性Departments和 Employees进行赋值进而实现对DropDownList和GridView进行绑定,通过属性SelectedDepartment得到用户选择 的筛选部门。为了尽可能让接口只暴露必须的信息,我们特意将对属性的读写作了控制。
1: public interface IEmployeeSearchView
2: {
3: IEnumerable<string> Departments { set; }
4: string SelectedDepartment { get; }
5: IEnumerable<Employee> Employees { set; }
6: }
7:
8: public partial class EmployeeSearchView: Page, IEmployeeSearchView
9: {
10: //其他成员
11: public IEnumerable<string> Departments
12: {
13: set
14: {
15: this.DropDownListDepartments.DataSource = value;
16: this.DropDownListDepartments.DataBind();
17: }
18: }
19: public string SelectedDepartment
20: {
21: get { return this.DropDownListDepartments.SelectedValue;}
22: }
23: public IEnumerable<Employee> Employees
24: {
25: set
26: {
27: this.GridViewEmployees.DataSource = value;
28: this.GridViewEmployees.DataBind();
29: }
30: }
31: }
虽然从可测试性的角度来说PV模式是一种不错的选择,因为所有的UI处理逻辑全部定义在Presenter上,意味着所有的UI处理逻辑都可以被测 试。但是我们需要将View可供操作的UI元素定义在对应的接口中,对于一些复杂的富客户端(Rich Client)View来说,接口成员将会变得很多,这无疑会提升编程所需的代码量。从另一方讲,由于Presenter需要在控件级别对View进行细 粒度的控制,这无疑会提供Presenter本身的复杂度,往往会使原本简单的逻辑复杂化,在这种情况下我们往往采用SoC模式。
在SoC(Supervising Controller)模式下,为了降低Presenter的复杂度,我们将诸如数据绑定和格式化这样简单的UI处理逻辑逻辑转移到View中,这些处理 逻辑会体现在View实现的接口中。尽管View从Presenter中接管了部分UI处理逻辑,但是Presenter依然是整个三角关系的驱动 者,View被动的地位依然没有改变。对于用户作用在View上的交互操作,View本身并不进行响应,而是直接将交互请求转发给Presenter,后 者在独立完成相应的处理流程(可能涉及到针对Model的调用)之后会驱动View或者创建新的View作为对用户交互操作的响应。
View和Presenter交互的规则(针对SoC模式)
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的属性去获取。
实例演示:SoC模式的应用
为了让读者对MVP模式,尤其是该模式下的View和Presenter之间的交互方式具有一个深刻的认识,我们现在来进行一个简单的实例演示。本 实例采用上面提及的关于员工查询的场景,并且采用ASP.NET Web Form来建立这个简单的应用,最终呈现出来的效果如上图所示。前面我们已经演示了采用PV模式下的IView应该如何定义,现在我们来看看SoC模式下 的IView有何不同。先来看看表示员工信息的数据类型如何定义,我们通过具有如下定义的数据类型Employee来表示一个员工。简单起见,我们仅仅定 义了表示员工基本信息(ID、姓名、性别、出生日期和部门)的5个属性。
1: public class Employee
2: {
3: public string Id { get; private set; }
4: public string Name { get; private set; }
5: public string Gender { get; private set; }
6: public DateTime BirthDate { get; private set; }
7: public string Department { get; private set; }
8:
9: public Employee(string id, string name, string gender, DateTime birthDate, string department)
10: {
11: this.Id = id;
12: this.Name = name;
13: this.Gender = gender;
14: this.BirthDate = birthDate;
15: this.Department = department;
16: }
17: }
作为包含应用状态和状态操作行为的Model通过如下一个简单的EmployeeRepository类型还体现。如代码所示,表示所有员工列表的 数据通过一个静态字段来维护,而GetEmployees返回指定部门的员工列表。如果没有指定筛选部门或者指定的部门字符为空,则直接返回所有的员工列 表。
1: public class EmployeeRepository
2: {
3: private static IList<Employee> employees;
4: static EmployeeRepository()
5: {
6: employees = new List<Employee>();
7: employees.Add(new Employee("001", "张三", "男", new DateTime(1981, 8, 24), "销售部"));
8: employees.Add(new Employee("002", "李四", "女", new DateTime(1982, 7, 10), "人事部"));
9: employees.Add(new Employee("003", "王五", "男", new DateTime(1981, 9, 21), "人事部"));
10: }
11: public IEnumerable<Employee> GetEmployees(string department = "")
12: {
13: if (string.IsNullOrEmpty(department))
14: {
15: return employees;
16: }
17: return employees.Where(e => e.Department == department).ToArray();
18: }
19: }
接下来我们来看作为View接口的IEmployeeSearchView的定义。如下面的代码片断所示,该接口定义了BindEmployees 和BindDepartments两个方法,分别用于绑定基于部门列表的DropDownList和基于员工列表的DataView。除此之 外,IEmployeeSearchView接口还定义了一个事件DepartmentSelected,该事件会在用户选择了筛选部门后点击“查询”按 钮时触发。DepartmentSelected事件参数类型为自定义的DepartmentSelectedEventArgs,属性 Department表示用户选择部门。
1: public interface IEmployeeSearchView
2: {
3: void BindEmployees(IEnumerable<Employee> employees);
4: void BindDepartments(IEnumerable<string> departments);
5: event EventHandler<DepartmentSelectedEventArgs> DepartmentSelected;
6: }
7:
8: public class DepartmentSelectedEventArgs : EventArgs
9: {
10: public string Department { get; private set; }
11: public DepartmentSelectedEventArgs(string department)
12: {
13: Guard.ArgumentNotNullOrEmpty(department, "department");
14: this.Department = department;
15: }
16: }
作为MVP三角关系核心的Presenter通过具有如下定义的EmployeeSearchPresenter表示。如下面的代码片断所示,表示 View的只读属性类型为IEmployeeSearchView接口,而另一个只读属性Repository则表示作为Model的 EmployeeRepository对象,两个属性均在构造函数中初始化。
1: public class EmployeeSearchPresenter
2: {
3: public IEmployeeSearchView View { get; private set; }
4: public EmployeeRepository Repository { get; private set; }
5:
6: public EmployeeSearchPresenter(IEmployeeSearchView view)
7: {
8: this.View = view;
9: this.Repository = new EmployeeRepository();
10: this.View.DepartmentSelected += OnDepartmentSelected;
11: }
12: public void Initialize()
13: {
14: IEnumerable<Employee> employees = this.Repository.GetEmployees();
15: this.View.BindEmployees(employees);
16: string[] departments = new string[] { "销售部", "采购部", "人事部", "IT部" };
17: this.View.BindDepartments(departments);
18: }
19: protected void OnDepartmentSelected(object sender, DepartmentSelectedEventArgs args)
20: {
21: string department = args.Department;
22: var employees = this.Repository.GetEmployees(department);
23: this.View.BindEmployees(employees);
24: }
25: }
在构造函数中我们注册了View的DepartmentSelected事件,作为事件处理器的OnDepartmentSelected方法通过 调用Repository(即Model)实现了针对所选部门的筛选,而返回的员工列表通过调用View的BindEmployees方法实现了在 View上的数据绑定。在Initialize方法中,我们通过调用Repository获取了表示所有员工的列表,并通过View的 BindEmployees方法显示在界面上;通过调用View的BindDepartments方法将作为筛选条件的部门列表绑定在View上。
最后我们来看看作为View的Web页面如何定义,如下所示的是作为页面主体部分的HTML,核心部分之包括一个用于绑定筛选部门列表的DropDownList和一个绑定员工列表的GridView。
1: <html xmlns="http://www.w3.org/1999/xhtml">
2: <head runat="server">
3: ...
4: </head>
5: <body>
6: <form id="form1" runat="server">
7: <div id="page">
8: <div class="top">
9: 选择查询部门:
10: <asp:DropDownList ID="DropDownListDepartments" runat="server" />
11: <asp:Button ID="ButtonSearch" runat="server" Text="查询" OnClick="ButtonSearch_Click" />
12: </div>
13: <asp:GridView ID="GridViewEmployees" runat="server" AutoGenerateColumns="false" Width="100%">
14: <Columns>
15: <asp:BoundField DataField="Name" HeaderText="姓名" />
16: <asp:BoundField DataField="Gender" HeaderText="性别" />
17: <asp:BoundField DataField="BirthDate" HeaderText="出生日期" DataFormatString="{0:dd/MM/yyyy}" />
18: <asp:BoundField DataField="Department" HeaderText="部门"/>
19: </Columns>
20: </asp:GridView>
21: </div>
22: </form>
23: </body>
24: </html>
如下所示的是该Web页面的后台代码的定义。它实现了定义在IEmployeeSearchView接口的两个方法(BindEmployees和 BindDepartments)和一个事件(DepartmentSelected)。表示Presenter的同名属性在构造函数中被初始化。在页面 加载的时候(Page_Load方法)Presenter的Initialize方法被调用,而在“查询”按钮被点击的时候 (ButtonSearch_Click)事件DepartmentSelected被触发。
1: public partial class Default : Page, IEmployeeSearchView
2: {
3: public EmployeeSearchPresenter Presenter { get; private set; }
4: public event EventHandler<DepartmentSelectedEventArgs> DepartmentSelected;
5: public Default()
6: {
7: this.Presenter = new EmployeeSearchPresenter(this);
8: }
9: protected void Page_Load(object sender, EventArgs e)
10: {
11: if (!this.IsPostBack)
12: {
13: this.Presenter.Initialize();
14: }
15: }
16: public void BindEmployees(IEnumerable<Employee> employees)
17: {
18: this.GridViewEmployees.DataSource = employees;
19: this.GridViewEmployees.DataBind();
20: }
21: public void BindDepartments(IEnumerable<string> departments)
22: {
23: this.DropDownListDepartments.DataSource = departments;
24: this.DropDownListDepartments.DataBind();
25: }
26: protected void ButtonSearch_Click(object sender, EventArgs e)
27: {
28: string department = this.DropDownListDepartments.SelectedValue;
29: DepartmentSelectedEventArgs eventArgs = new DepartmentSelectedEventArgs(department);
30: if (null != DepartmentSelected)
31: {
32: DepartmentSelected(this, eventArgs);
33: }
34: }
35: }
二、Model2
Trygve M. H. Reenskau当初提出的MVC是作为基于GUI的桌面应用的架构模式,并不太适合Web本身的特性。虽然MVC/MVP也可以直接用于ASP.NET Web Form应用,但这是因为微软基于桌面应用的编程模式 来设计基于Web Form的ASP.NET应用框架的。Web应用不同于GUI桌面应用在于用户是通过浏览器与应用进行交互,交互请求和相应是通过HTTP请求和回复来完 成的。
为了让MVC能够Web应用提供原生的支持,另一个被称为Model2 的MVC变体被提出来,Model2来源于基于Java的Web应用架构模式。Java Web应用具有两种基本的架构模式,分别被称为Model1和Model2。Model1类似于我们前面提及的自治试图模式,它将数据的可视化呈现和用户 交互操作的处理逻辑合并在一起。Model1使用于那些比较简单的Web应用,对于相对复杂的应用应该采用Model 2。
为了让开发者采用相应的编程模式进行GUI桌面应用和Web应用的开发,微软通过ViewState和Postback对背后的HTTP请求和回复 机制进行了封装,使我们能够像编写Windows Forms应用一样采用事件驱动的方式进行ASP.NET Web Forms应用的编程。而Model 2采用完全不同的设计,它让开发者直接面向Web,让他们关注HTTP的请求和回复流程,所以Model 2提供对Web应用原生的支持。
对于Web应用来说,和用户直接交互的UI界面由浏览器来提供,接下来我们详细讨论作为MVC的三要素是如何相互协作对从浏览器发出的用户交互请求的响应的,下图所示的序列图体现了整个流程的全过程。
Model 2种一个HTTP请求的目标是Controller中的某个Action,后者体现为定义在Controller类型中的某个方法,所以对请求的处理最终 体现在对Controller对象的激活和对Action方法的执行。一般来说,Controller、Action以及作为Action方法的部分参数 (针对HTTP-GET)可以直接通过请求的URL解析出来。
如上图所示,我们通过一个拦截器(Interceptor)对抵达Web服务器的HTTP请求进行拦截。一般的Web应用框架都提供了针对这样一种 拦截机制,对于ASP.NET来说,我们可以以HttpModule的形式来定义这么一个拦截器。拦截器根据请求解析出目标Controller和对应的 Action,Controller被激活之后Action方法被执行。对于需要传入Action方法的输入参数,则来源于请求地址或/和Post的数 据。
在Controller的Action方法被执行过程中,它可以调用Model获取或者改变其状态。在Action方法执行的最后阶段会选择相应的 View,绑定在View上的数据来源Model或者基于显示要求进行得简单逻辑计算,我们有时候它们成为VM(View Model),即基于View的Model(MVC中的Model是与UI无关的)。生成的View最终写入HTTP回复并最终呈现在用户的浏览器中。
和MVP一样,Model 2完全隔断了View和Model之间的联系。Controller作为支配者地位在Model 2体现尤为明显,用户交互请求不再由View报告给Controller(Presenter),而是由拦截器直接转发给Controller。 Controller不仅仅决定着Model的调用,还决定了View的选择和生成。ASP.NET MVC就是基于Model 2模式设计的。
参考资料:
1、Dino Esposito,Andrea Saltarello《Micorsoft .NET Architecting Applications for Enterprise》
2、Adam Freeman, Steven Sanderson《Pro ASP.NET MVC 3 Framework》
3、Martin Fowler 《GUI Architectures》: http://martinfowler.com/eaaDev/uiArchs.html
4、Martin Fowler 《Passive View》:http://martinfowler.com/eaaDev/PassiveScreen.html
5、Martin Fowler 《Supervising Controller》:http://martinfowler.com/eaaDev/SupervisingPresenter.html
6、Mike Potel,VP & CTO,Taligent, Inc. 《MVP: Model-View-Presenter The Taligent Programming Model for C++ and Java》
7、Model View Controller- Wikipedia, the free encyclopedia:http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
8、Model View Presenter- Wikipedia, the free encyclopedia:http://en.wikipedia.org/wiki/Model_View_Presenter
9、Steve Burbeck, Ph.D. 《Applications Programming in Smalltalk-80(TM):How to use Model-View-Controller (MVC)》:http://st-www.cs.illinois.edu/users/smarch/st-docs/mvc.html