使用ASP.NET实现Model View Presenter(MVP)
作者:Billy McCafferty 翻译:张善友
原文地址:http://www.codeproject.com/useritems/ModelViewPresenter.asp
这篇文章描述了ASP.NET 2.0使用Model-View-Presenter 模式实现业务逻辑与表现层的适当分离。
- Download trivial example of MVP - 18 Kb
- Download simple Event-Handling MVP - 19 Kb
- Download sample MVP Enterprise Solution - 2.6 Mb
概述
经过多年代的ASP代码积累,微软开发了具有一流水平的网络平台:ASP.NET. ASP.NET使用后置代码页面方式隔离业务逻辑。虽然用心良苦,但是ASP.NET在企业级应用开发方面还是存在如下的不足:
l 后 置代码页中混合了表现层,业务逻辑层,数据访问层的代码。之所以出现这种情况是因为后置代码充当了事件引发,流程控制,业务规则和表现逻辑,业务逻辑和数 据访问的协调者等多种角色。后置代码页充当这么多的职责导致许多难处理的代码。在企业应用中,一个良好的设计原则是各层之间的适当分离和保持后置代码页内 容的尽可能干净。使用Model-View-Presenter 模式,后置代码的内容将非常简单,严格的管理表现层内容。
l 后置代码模型的另一个缺点是它难以不借助帮助类/工具类实现重用后置代码页面之间的可重用代码。很明显的,这也是提供了一个适当的解决方案,但往往导致ASP式的类,不像是一流的对象。通过适当的设计,每个类都应有清晰的职责,通常一个叫
ContainsDuplicatePresentationCodeBetweenThisAndThat.cs并不合适
l 最后,对后置代码页进行单元测试非常困难因为它们同表现层的太紧密了,当然可以选择NUnitASP这样的工具,但是他们非常的耗费时间,并且难以维护。单元测试应当是简单快速的。
可以采用各种技术手段是后置代码页保持分离。例如Castle MonoRail项目仿效Ruby-On-Rails , 但是放弃了ASP.NET的事件模型。Maverick.NET是一个支持ASP.NET事件模型的框架但是保留后置代码页作为程序的控制器。理想的解决 方案是使用ASP.NET的事件模型并保持后置代码页的尽可能简单。Model-View-Presenter 模式是一个不需要借助第三方框架实现这个目标。
Model-View-Presenter
Model-View-Presenter (MVP) 模式是 Model-View-Controller (MVC) 模式的变种,针对事件模型,像ASP.NET这样的框架。具体参看简单介绍GUI设计模式(MVP)。MVP最初使用与Dolphin Smalltalk. 主要的变化是Presenter实现MVC的Observer设计,基本设计和MVC相同:Model存储数据,View表示Model的表 现,Presenter协调两者之间的通信。在 MVP 中 view 接收到事件,然后会将它们传递到 Presenter, 如何具体处理这些事件,将由 Presenter 来完成。关于在 MVC 和 MVP的深入比较,请查看
http://www.darronschall.com/weblog/archives/000113.cfm.,接下来以三个例子详细说明MVP模式。
最简单的例子
这个例子,客户想在页面上显示当前的时间(从简单的开始容易理解)。显示时间的ASPX页面是“View”。Presenter负责决定现在的时间(Model),而且把Model告知 View。我们从一个单元测试开始。
[TestFixture]
public class CurrentTimePresenterTests {
[Test]
public void TestInitView() {
MockCurrentTimeView view = new MockCurrentTimeView();
CurrentTimePresenter presenter = new CurrentTimePresenter(view);
presenter.InitView();
Assert.IsTrue(view.CurrentTime > DateTime.MinValue);
}
private class MockCurrentTimeView : ICurrentTimeView {
public DateTime CurrentTime {
set { currentTime = value; }
// This getter won't be required by ICurrentTimeView,
// but it allows us to unit test its value.
get { return currentTime; }
}
private DateTime currentTime = DateTime.MinValue;
}
}
上 面的单元测试代码和右边的类图,描述了MVP各个元素之间的关系。单元测试中创建的第一个对象实例是MockCurrentTimeView,从这个单元 测试中可以看出,所有的表现逻辑的单元测试并没有一个ASPX页面(View),所需要的是一个实现视图接口的对象;因此可以创建一个视图的模拟对象 (Mockview)代替真正的视图对象。
下一行代码创建了一个Presenter的对象实例,通过它的构造函数传递了一个实现ICurrentTimeView接口的对象,这样,Presenter现在能够操作View,从类图中可以看出,Presenter只与View的接口通信。这允许实现相同的View接口的多个View被Presenter使用。
最后,Presenter调用InitView()方法,这个方法将获取当前的时间并通过公开的属性ICurrentTimeView传递给视图(View),单元测试断言CurrentTime的值应比它的初始值大(如果需要可以做更多的断言)。
那么现在要做的就是要运行单位测试并通过了!
ICurrentTimeView.cs – 视图接口
使单元测试编译通过的第一步是创建ICurrentTimeView.cs,这个接口提供Presenter 和 View之间的沟通桥梁,在这个例子中,视图接口需要暴露一个Model数据,使Persenter能够将Model(当前时间)传递给View。
public interface ICurrentTimeView {
DateTime CurrentTime { set; }
}
因为只需要显示模型数据,视图接口中只需要一个CuttentTime的Set;但是设置了一个Get,用于在单元测试中获取视图的CurrentTime,它也可以添加到MockCurrentTimeView而不要在接口中定义,这样,在视图接口中暴露的接口属性不需要定义getter/setter(上面的单元测试就使用了这个技术)。
CurrentTimePresenter.cs - The Presenter
Presenter处理同Model之间的逻辑并将Model传递给View。要使单元测试通过编译,Presenter的实现代码如下:
public class CurrentTimePresenter {
public CurrentTimePresenter(ICurrentTimeView view) {
if (view == null) throw new ArgumentNullException("view may not be null");
this.view = view;
}
public void InitView() {
view.CurrentTime = DateTime.Now;
}
private ICurrentTimeView view;
}
完成上述代码,我们就完成了Unit Test,mock view,Presenter和View.单元测试现在可以成功编译并通过。下一个步骤是创建ASPX页面充当真正的View。
注意到ArgumentNullException异常的检查,这项技术被称为基于契约设计(Design By Contract),在代码中象这样做必要的检查可以大大的降低Bug的数量。关于基于契约设计(Design By Contract)的更多信息请参考http://archive.eiffel.com/doc/manuals/technology/contract和http://www.codeproject.com/csharp/designbycontract.asp.
ShowMeTheTime.aspx - The View
这个页面需要做以下内容:
l ASPX页面需要提供一个方法显示当前的时间,用一个Label控件显示时间
l 后置代码必须实现接口IcurrentTimeView
l 后置代码必须创建一个Presenter对象,并把自己传递给它的构造函数
l 创建好Persenter对象后,需要调用InitView()
ASPX 页面:
<asp:Label id="lblCurrentTime" runat="server" />
...
<
ASPX 后置代码页面:
public partial class ShowMeTheTime : Page, ICurrentTimeView
{
protected void Page_Load(object sender, EventArgs e) {
CurrentTimePresenter presenter = new CurrentTimePresenter(this);
presenter.InitView();
}
public DateTime CurrentTime {
set { lblCurrentTime.Text = value.ToString(); }
}
}
那就是MVP?
总 而言之,是的,但是很有很多的内容。上面这个例子给你的不好印象是这么小的功能需要做那么多的工作。我们已经从创建ASPX页面到一个Presenter 类,一个View接口和一个单元测试类……,我们获得的好处是对Presenter的单元测试,也就是很容易的对后置代码页面进行单元测试。这是一个最简 单的例子就像写“Hello World”这样。当构建企业级应用程序的时候就会体现出MVP模式的好处。下面的主题是企业级的ASP.NET应用中使用MVP模式。
在企业级ASP.NET应用中使用MVP
l 使用用户控件封装Views:这个主题讨论用户控件作为MVP中的View
l MVP的事件处理:这个主题讨论连同页面验证传递事件到Presenter,IsPostBack和将消息传递到View
l MVP和PageMethods的页面重定向:这个主题讨论使用用户控件作为View,如何使用PageMethods处理页面重定向。
l MVP的Presentation安全控制:这个主题讨论如何根据基本的安全限制显示/掩藏View中的区段
l 使用MVP的应用的架构(高级):这是个重点,这个主题展示一个使用Nhibernate作为数据访问层的MVP应用。
使用用户控件封装Views
在上面的例子中,ASPX页面充当View,把ASPX页面做View只有一个简单的目的—显 示当前的时间。但是在一个比较有代表性的应用中,一个页面通常包含一个或者多个功能性的区段,他们可能是WebPart,用户控件等等。在企业级应用中, 保持功能性的分离以及很容易的从一个地方移动到另一个地方是非常重要的。使用MVP,用户控件用于封装View,ASPX作为 “View Initializers”和页面的重定向。扩展上面的例子,只要修改ASPX页面的实现。这也是MVP的另一个好处,许多变化可以限制在View层而不 要修改Presenter和Model。
ShowMeTheTime.aspx Redux - The View Initializer
用这种新的方式,ShowMeTheTime.aspx负责下列各项:
1. ASPX上面需要声明实现ICurrentTimeView接口的用户控件
2. 后置代码必须创建一个Presenter对象,并把用户控件传递给它的构造函数
3. 创建好Persenter对象后,需要调用InitView()
ASPX 页面:...
<%@ Register TagPrefix="mvpProject" TagName="CurrentTimeView" Src="./Views/CurrentTimeView.ascx" %>
<mvpProject:CurrentTimeView id="currentTimeView" runat="server" />
...
The ASPX 后置代码页面:
public partial class ShowMeTheTime : Page // No longer implements ICurrentTimeView
{
protected void Page_Load(object sender, EventArgs e) {
InitCurrentTimeView();
}
private void InitCurrentTimeView() {
CurrentTimePresenter presenter = new CurrentTimePresenter(currentTimeView);
presenter.InitView();
}
}
CurrentTimeView.ascx – 用户控件作为View
用户控件现在充当View,完全取决于我们所期望的View是什么样的
The ASCX 页面:...
<asp:Label id="lblCurrentTime" runat="server" />
...
ASCX 后置代码页面:
public partial class Views_CurrentTimeView : UserControl, ICurrentTimeView
{
public DateTime CurrentTime {
set { lblCurrentTime.Text = value.ToString(); }
}
}
使用用户控件作为View的利弊
使用用户控件作为MVP的View的主要缺点是添加另一个元素的方式。现在MVP元素由以下元素组成:unit test, presenter, view interface, view implementation (the user control) 和the view initializer (the ASPX page).使用用户控件作为View的好处如下:
l View非常容易的从一个页面移到另一个页面,这是大型的应用程序中经常发生的事
l View在不需要复制代码就可以在不同的页面之间重用
l View 可以在不同的aspx页面中进行初始化。例如一个用于显示项目列表的用户控件。在站点的报表区域用户可能看并且可以过滤数据。在站点的另一个区域用户只能 看部分数据和不能使用过滤器。在实现方面,同一个View可以传给相同的Presenter,但是不同的Aspx页面可以调用Presenter的不同方 法初始化View
l 添加其他View到ASPX页面并不需要额外的代码,只需要将用户控件添加到页面,然后在后置代码中把它和他的Presenter连接在一起就可以了。在同一页面中没有使用用户控件管理不同的功能性区段,很快就会出现维护困难的问题。
MVP的事件处理
上 面的例子,本质上描述的是一个Presenter同它的View之间的单向的通信。Presenter同Model通信,并把它传递给View。大多数情 况下,引发的事件需要Presenter进行处理。此外一些事件依赖于页面上的验证是否通过或者是IsPostBack。例如数据绑定,在 IsPostBack的时候不能被引发。
声明:Page.IsPostBack和Page.IsValid是Web特有的。下面所讨论的Presenter层只在Web环境中有效。但是只要做小小的修改,也能很好工作在Webform,Winform和Mobile应用中。无论如何,他们的理论基础都是一样的。
简单的事件处理序列图
继续上面的例子,用户可能要给当前时间上增加几天,然后在View中显示更新的时间,假设用户输入的是有效的数字,View中显示的时间应等于当前时间加上增加的天数。当不是IsPostBack的时候,View显示的事当前时间,当IsPostBack的时候,Presenter应当对事件作出回应。下面的序列图表示了用户的初始请求(上面部分)和用户点击按钮”Add days”之后发生了什么.。
A)创建用户控件
这 一步只是表示ASPX页面中声明的用户控件。在页面初始化的时候,用户控件被创建。在图中表示的是实现接口IcurrentTimeView的用户控件。 在ASPX页面的后置代码的Page_Load事件,Presenter创建了一个实例,用户控件作为参数通过构造函数传递给Presenter,到此为 止,所有的描述的内容都和“使用用户控件封装Views”的一样。
B) Presenter 添加到View
为 了使事件能够从View(用户控件)传递到Presenter。View必须包含一个CurrentTimePresenter对象的引用,为了实现这个 目的,View Initializer, ShowMeTheTime.aspx将Presnter传递给View。这不会造成Presenter和View之间的依赖,Presenter依赖于 View的接口,View依赖于Presenter对事件的处理,让我们代码中看他们是如何工作的。
ICurrentTimeView.cs - The View Interface
public interface ICurrentTimeView {
DateTime CurrentTime { set; }
string Message { set; }
void AttachPresenter(CurrentTimePresenter presenter);
}
<A name=EventHandlingPresenter>CurrentTimePresenter.cs - The
Presenter</A></H3><PRE>public class CurrentTimePresenter {
public CurrentTimePresenter(ICurrentTimeView view) {
if (view == null) throw new ArgumentNullException("view may not be null");
this.view = view;
}
public void InitView(bool isPostBack) {
if (! isPostBack) {
view.CurrentTime = DateTime.Now;
}
}
public void AddDays(string daysUnparsed, bool isPageValid) {
if (isPageValid) {
view.CurrentTime = DateTime.Now.AddDays(double.Parse(daysUnparsed));
}
else {
view.Message = "Bad inputs...no updated date for you!";
}
}
private ICurrentTimeView view;
}
CurrentTimeView.ascx - The View
The ASCX Page:..
<asp:Label id="lblMessage" runat="server" /><br />
<asp:Label id="lblCurrentTime" runat="server" /><br />
<br />
<asp:TextBox id="txtNumberOfDays" runat="server" />
<asp:RequiredFieldValidator ControlToValidate="txtNumberOfDays" runat="server"
ErrorMessage="Number of days is required" ValidationGroup="AddDays" />
<asp:CompareValidator ControlToValidate="txtNumberOfDays" runat="server"
Operator="DataTypeCheck" Type="Double" ValidationGroup="AddDays"
ErrorMessage="Number of days must be numeric" /><br />
<br />
<asp:Button id="btnAddDays" Text="Add Days" runat="server"
OnClick="btnAddDays_OnClick" ValidationGroup="AddDays" />
...
The ASCX Code-Behind Page:
public partial class Views_CurrentTimeView : UserControl, ICurrentTimeView {
public void AttachPresenter(CurrentTimePresenter presenter) {
if (presenter == null) throw new ArgumentNullException("presenter may not be null");
this.presenter = presenter;
}
public string Message {
set { lblMessage.Text = value; }
}
public DateTime CurrentTime {
set { lblCurrentTime.Text = value.ToString(); }
}
protected void btnAddDays_OnClick(object sender, EventArgs e) {
if (presenter == null) throw new FieldAccessException("presenter has not yet been initialized");
presenter.AddDays(txtNumberOfDays.Text, Page.IsValid);
}
private CurrentTimePresenter presenter;
}
ShowMeTheTime.aspx - The View Initializer
The ASPX Page:...
<%@ Register TagPrefix="mvpProject" TagName="CurrentTimeView" Src="./Views/CurrentTimeView.ascx" %>
<mvpProject:CurrentTimeView id="currentTimeView" runat="server" />
...
The ASPX Code-Behind Page:
public partial class ShowMeTheTime : Page // No longer implements ICurrentTimeView
{
protected void Page_Load(object sender, EventArgs e) {
InitCurrentTimeView();
}
private void InitCurrentTimeView() {
CurrentTimePresenter presenter = new CurrentTimePresenter(currentTimeView);
currentTimeView.AttachPresenter(presenter);
presenter.InitView(Page.IsPostBack);
}
}
C) Presenter InitView
如 需求所定义的,如果不是IsPostBack,Presenter只是显示当前的时间。Presenter要知道在IsPostBack的时候该做些什 么,这不应该由Aspx的后置代码来决定。在上面的代码中你看到了Aspx的后置代码中没有IsPostBack的处理。它只是简单将值传给 Presenter,由Presenter来决定执行什么样的动作。
这可能导致一个问题:“如果是另一个用户控件引发的Post-back将会发生什么呢”。在这个例子中,当前的时间会保存在Label控件的ViewState中而再次显示在Label控件上,这些都依赖客户的需要。总体上,这是一个Presenter的好问题 –另一个用户控件引发的Post-back对这个用户控件的影响。即使你没有使用MVP,也是一个好问题。