带 ASP.NET 的模型-视图-表示器






4.93/5 (181投票s)
2006 年 7 月 3 日
24分钟阅读

1549219

27183
本文介绍了如何在 ASP.NET 2.0 中使用模型-视图-表示器模式,以促进表示层和业务逻辑之间的适当关注点分离
作者注:2007 年 10 月 15 日补充 - 建议使用 MVC 而非 MVP
随着我们作为开发人员的进步,我们努力寻找执行我们技艺的“最佳”方式。实现这一崇高目标的所选方法总是伴随着许多开发上的权衡。有些技术可能会简化代码但减少细粒度控制,而另一些则可以实现更大的能力同时引入复杂性。带 ASP.NET 的模型-视图-表示器是后者的一个绝佳例子。MVP 旨在促进 ASP.NET 应用程序的测试驱动开发,但遗憾的是增加了复杂性。因此,尽管开发人员将对他们的产品质量更有信心,但 MVP 阻碍了他们轻松维护代码的能力。是的,测试将(希望)通知开发人员引入了一个错误,但 MVP 固有的复杂性使得后来的团队成员难以熟悉代码库并在项目开发继续进行时维护它。
幸运的是,随着我们领域时间的快速推移,资源和工具变得可用,它们增强了我们编写强大应用程序的能力,同时简化了编码过程本身。例如,NHibernate 的引入消除了大量的数据访问代码,同时仍然为管理事务和动态查询数据提供了强大的功能。Castle MonoRail(以及Microsoft 即将推出的衍生框架)现在为编写可测试和可维护的 .NET Web 应用程序所做的工作,正如 NHibernate(以及即将推出的LINQ to Entities)为 ADO.NET 所做的工作一样。这并不是说以前的技术必然是错误的,而是说它们只适用于当时可用的开发人员工具集。
为了适应我们领域的发展,开发人员必须注意何时一项公认的技术在当前替代方案面前不再有价值。具体来说,MVP 曾是编写从零开始的测试驱动 ASP.NET 应用程序的强大技术,但与 Castle MonoRail 和 Microsoft 即将推出的 MVC 框架节省时间的好处和简单性相比,它不再是值得考虑的强有力候选者。奇怪的是,有时很难“放弃”以前运行完美的东西,但这就是我们行业的本质……一个不太可能很快改变的原则。
关于本文,我相信对于那些维护基于 MVP 构建的遗留应用程序的人以及那些有兴趣学习坚实领域驱动架构的人来说,它仍然具有持续的价值,该架构在下文和另一篇文章中进行了详细讨论。
总之,尽管我仍然认为 MVP 是开发全新 ASP.NET 解决方案的最佳技术,但我相信有现成的框架可以使整个工作变得简单得多。
引言
经过多年维护数千行 ASP 意大利面条代码之后,微软终于为我们提供了一个一流的 Web 开发平台:ASP.NET。ASP.NET 通过引入代码隐藏页,立即实现了表示层和业务逻辑之间的基本关注点分离。尽管初衷良好,并且对于基本应用程序来说非常完美,但在开发企业级 Web 应用程序时,代码隐藏页在许多方面仍然存在不足
- 代码隐藏页面容易将表示层、业务逻辑和数据访问代码混合在一起。这是因为代码隐藏页面通常充当事件处理程序、工作流控制器、表示层和业务规则之间的中介,以及表示层和数据访问代码之间的中介。赋予代码隐藏页面如此多的职责通常会导致代码难以管理。在企业应用程序中,良好设计的原则是维护层之间适当的关注点分离,并使代码隐藏页面尽可能简洁。通过模型-视图-表示器,我们将看到代码隐藏页面大大简化,并且严格用于管理表示细节。
- 代码隐藏模型 的另一个缺点是,在不使用整合重复代码的辅助/实用程序类的情况下,很难在代码隐藏页面之间重用演示逻辑。显然,有时这提供了一个足够的解决方案。然而,它常常导致不连贯的类,这些类更像 ASP 包含而非一流对象。通过适当的设计,每个类都应该具有内聚性并有一个明确的目的。一个名为 ContainsDuplicatePresentationCodeBetweenThisAndThat.cs 的类通常不符合条件。
- 最后,对代码隐藏页进行适当的单元测试几乎是不可能的,因为它们与表示层密不可分。可以使用 NUnitAsp 等选项,但它们实现耗时且难以维护。它们还会大大降低单元测试性能,而单元测试应该始终非常快。
可以采用各种技术来促进代码隐藏页面更好地分离关注点。例如,Castle MonoRail 项目试图模仿 Ruby-On-Rails 的一些优点,但在过程中放弃了 ASP.NET 事件模型。Maverick.NET 是一个可选支持 ASP.NET 事件模型的框架,但在此过程中将代码隐藏页面保留为控制器。理想情况下,应该采用一种利用 ASP.NET 事件模型,同时仍使代码隐藏页面尽可能简单的解决方案。模型-视图-表示器模式正是这样做的,而无需依赖第三方框架来促进这一目标。
模型-视图-表示器
模型-视图-表示器(MVP)是模型-视图-控制器(MVC)模式的一种变体,但专门针对像 ASP.NET 这样的页面事件模型。回顾一下历史,MVP 最初是 Dolphin Smalltalk 背后的首选框架。MVP 的主要区别在于,表示器实现了 MVC 的观察者设计,但 MVC 的基本思想保持不变:模型存储数据,视图显示模型的表示,表示器协调层之间的通信。MVP 采用观察者方法,即表示器解释事件并执行必要的逻辑,将这些事件映射到操作模型的正确命令。有关 MVC 与 MVP 的更多信息,请参阅 Darron Schall 关于该主题的简洁条目。接下来是对 MVP 的详细考察,以三个示例项目为例。
作者注:Martin Fowler 建议将 MVP 拆分为两个“新”模式,即 Supervising Controller 和 Passive View。有关拆分的简短概述,请转到此处。此处描述的内容更符合 Supervising Controller,因为视图感知模型。
最简单的例子
在此示例项目中,客户需要一个显示当前时间的页面。谢天谢地,他们从简单的东西开始!显示时间的 ASPX 页面是“视图”。“表示器”负责确定当前时间——即“模型”——并将模型提供给视图。一如既往,我们从单元测试开始
[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 的实例。如本单元测试所示,所有 Presenter 逻辑都可以在没有 ASPX 页面(即 View)的情况下进行单元测试。所需要的只是一个实现 View 接口的对象;因此,创建了一个模拟视图来替代“真实”视图。
下一行创建了一个表示器的实例,通过其构造函数传递了一个实现 ICurrentTimeView
的对象。通过这种方式,表示器现在可以操作视图。如图所示,表示器只与视图接口通信。它不直接与具体实现交互。这允许多个视图(实现相同的视图接口)被同一个表示器使用。
最后,请求 Presenter 执行 InitView()
。此方法将获取当前时间并通过 ICurrentTimeView
公开的公共属性将其传递给 View。然后进行单元测试断言,即 View 上的 CurrentTime
应大于其初始值。如果需要,当然可以进行更详细的断言。
现在所需要做的就是让单元测试编译并通过!
ICurrentTimeView.cs:视图接口
为了让单元测试编译通过,第一步应该创建 ICurrentTimeView.cs。这个视图接口将提供表示器和视图之间的通信渠道。在当前情况下,视图接口需要公开一个公共属性,表示器可以使用该属性将当前时间(模型)传递给视图。
public interface ICurrentTimeView
{
DateTime CurrentTime { set; }
}
视图只需要当前时间的 setter,因为它只需要显示模型,但提供 getter 允许在单元测试中检查 CurrentTime
。因此,与其向接口添加 getter,不如将其添加到 MockCurrentTimeView
,根本不需要在接口中定义。这样,可以对视图的暴露属性进行单元测试,而无需强制在视图接口中定义多余的 setter/getter。上面描述的单元测试展示了这种技术。
CurrentTimePresenter.cs:表示器
表示器将处理与模型通信并将模型值传递给视图的逻辑。要使单元测试编译并通过所需的表示器如下。
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;
}
一旦上述各项(单元测试、模拟视图、视图和表示器)开发完成,单元测试现在将成功编译并通过。下一步是创建一个 ASPX 页面作为实际视图。快速旁注,请注意 ArgumentNullException
检查。这是一种名为“契约式设计”的技术。在代码中随处添加这样的检查将大大减少查找错误的麻烦。有关契约式设计的更多信息,请参阅本文和本文。
ShowMeTheTime.aspx:视图
实际视图需要执行以下操作
- ASPX 页面需要提供一种显示当前时间的方法。如下所示,将使用一个简单的标签进行显示。
- 代码隐藏必须实现
ICurrentTimeView
。 - 代码隐藏需要创建 Presenter,并将其自身传递给 Presenter 的构造函数。
- 创建 Presenter 后,需要调用
InitView()
来完成 MVP 周期。
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 进行单元测试,即能够方便地对通常在代码隐藏页面中找到的代码进行单元测试。与简单示例一样,MVP 的优势在开发和维护企业级 Web 应用程序时才显现出来,而不是在编写“Hello World”之类的示例时。以下主题详细阐述了 MVP 在企业 ASP.NET 应用程序中的用法。
企业 ASP.NET 应用程序中的 MVP
- 使用用户控件封装视图:本主题讨论用户控件作为视图的 MVP 方法。
- MVP 中的事件处理:本主题讨论将事件传递给表示器,并考虑页面验证、IsPostBack 以及向视图发送消息。
- 使用 MVP 和 PageMethods 进行页面重定向:本主题包括如何使用 PageMethods 和用户控件作为视图架构来处理页面重定向。
- 使用 MVP 进行演示文稿安全:本主题讨论处理用于隐藏/显示视图部分的 基本安全约束。
- MVP 应用程序架构(高级):全部!本主题提出了一个构建 MVP 企业级应用程序的推荐方法,该应用程序配备了 NHibernate 数据访问层。
一、用用户控件封装视图
在前面的简单示例中,ASPX 页面本身充当视图。以这种方式对待 ASPX 是足够的,因为该页面只有一个简单的目的——显示当前时间。但在更具代表性的项目中,通常情况下,单个页面将具有一个或多个功能部分,无论是 WebParts、用户控件等。在这些更典型的企业应用程序中,保持功能逻辑分离并使其易于将功能从一个区域移动/复制到另一个区域非常重要。通过 MVP,用户控件可以用于封装视图,而 ASPX 页面充当“视图初始化器”和页面重定向器。扩展前面的示例,我们只需要修改 ASPX 页面即可实现更改。这是 MVP 的另一个优点;可以对视图层进行许多更改,而无需修改表示器和模型层。
ShowMeTheTime.aspx 重做:视图初始化器
通过这种新方法,将用户控件用作视图,ShowMeTheTime.aspx 现在负责以下任务
- ASPX 页面需要声明将实现
ICurrentTimeView
的用户控件。 - ASPX 代码隐藏需要创建表示器,并将用户控件传递给表示器的构造函数。
- 在将 View 传递给 Presenter 后,ASPX 需要调用
InitView()
来完成 MVP 周期。
ASPX 页面
...
<%@ Register TagPrefix="mvpProject"
TagName="CurrentTimeView" Src="./Views/CurrentTimeView.ascx" %>
<mvpProject:CurrentTimeView id="currentTimeView" runat="server" />
...
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:用户控件即视图
用户控件现在代表最基本的视图。它尽可能地“笨拙”——这正是我们希望视图成为的样子。
ASCX 页面
...
<asp:Label id="lblCurrentTime" runat="server" />
...
ASCX 代码隐藏页面
public partial class Views_CurrentTimeView : UserControl, ICurrentTimeView
{
public DateTime CurrentTime
{
set { lblCurrentTime.Text = value.ToString(); }
}
}
用户控件即视图方法的优缺点
显然,“用户控件即视图”MVP 方法的主要缺点是它又增加了一个组成部分。整个 MVP 关系现在由:单元测试、表示器、视图接口、视图实现(用户控件)和视图初始化器(ASPX 页面)组成。增加这一额外的间接层增加了设计的整体复杂性。用户控件即视图方法的优点包括
- 视图可以轻松地从一个 ASPX 页面移动到另一个 ASPX 页面。这在中型到大型 Web 应用程序中经常发生。
- 视图可以轻松地被不同的 ASPX 页面重用,而几乎不需要复制任何代码。
- 视图可以由不同的 ASPX 页面以不同的方式初始化。例如,可以编写一个显示项目列表的用户控件。在站点的报告部分,用户可以查看和筛选所有可用的项目。在站点的另一个部分,用户可能只能查看项目的一个子集,并且没有运行筛选器的能力。在实现中,相同的视图可以传递给相同的表示器,但随后每个 ASPX 页面,在站点的各自部分,将调用表示器上的不同方法以独特的方式初始化视图。
- 可以在 ASPX 页面中添加额外的视图,而无需增加太多额外的编码开销。只需将新的用户控件作为视图包含到 ASPX 页面中,并在代码隐藏中将其链接到其表示器。在不使用用户控件的情况下,将多个功能部分放置在同一个 ASPX 页面中,很快就会造成维护难题。
二、MVP 的事件处理
前面的例子描述了 Presenter 和它的 View 之间本质上是单向的通信。Presenter 与 Model 通信,并将其传递给 View。在大多数情况下,会发生需要传递给 Presenter 处理的事件。此外,一些事件取决于表单是否有效以及是否发生了 IsPostBack。例如,有些操作(如数据绑定)在 IsPostBack 时可能无法完成。
免责声明:Page.IsPostBack 和 Page.IsValid 是特定于 Web 的关键字。因此,以下内容可能会使所描述的表示层在非 Web 环境中略微无效。然而,稍作修改,它将适用于 WebForms、WinForms 或移动应用程序。无论如何,理论是相同的,但我欢迎提出建议,使表示层可移植到任何 .NET 环境。
一个简单的事件处理序列
继续前面的示例,假设现在需求规定用户可以输入要添加到当前时间的天数。视图中显示的时间应更新为当前时间加上用户提供的天数,前提是用户提供了有效输入。当不是 IsPostBack 时,应显示当前时间。当是 IsPostBack 时,Presenter 应相应地响应事件。下面的序列图显示了用户首次请求时(图上半部分)发生的情况以及用户单击“添加天数”按钮时(图下半部分)发生的情况。图后将对序列进行更全面的回顾。

A) 创建用户控件即视图
此步骤仅代表 ASPX 页面中找到的内联用户控件声明。在页面初始化期间,用户控件被创建。它包含在图中,以强调用户控件实现 ICurrentTimeView
的事实。在 Page_Load 期间,ASPX 代码隐藏会创建一个 Presenter 实例,并通过其构造函数传递用户控件作为视图。到目前为止,一切看起来都与“使用用户控件封装视图”部分中描述的相同。
B) 表示器连接到视图
为了将事件从用户控件(视图)传递给表示器,它必须引用 CurrentTimePresenter
的实例。为此,视图初始化器 ShowMeTheTime.aspx 将表示器传递给视图以供以后使用。与最初的反应相反,这不会导致表示器和视图之间出现双向依赖。相反,表示器依赖于视图接口,视图实现依赖于表示器来传递事件。要了解它是如何工作的,让我们回顾一下所有组件现在是如何实现的。
ICurrentTimeView.cs:视图接口
public interface ICurrentTimeView
{
DateTime CurrentTime { set; }
string Message { set; }
void AttachPresenter(CurrentTimePresenter presenter);
}
CurrentTimePresenter.cs:表示器
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:视图
ASCX 页面
...
<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" />
...
ASCX 代码隐藏页面
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:视图初始化器
ASPX 页面
...
<%@ Register TagPrefix="mvpProject"
TagName="CurrentTimeView" Src="./Views/CurrentTimeView.ascx" %>
<mvpProject:CurrentTimeView id="currentTimeView" runat="server" />
...
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);
currentTimeView.AttachPresenter(presenter);
presenter.InitView(Page.IsPostBack);
}
}
C) Presenter InitView
根据需求定义,Presenter 只有在不是 IsPostBack 时才应该显示当前时间。需要注意的重要操作是 Presenter 应该根据 IsPostBack 来决定做什么。这不应该是 ASPX 代码隐藏的职责来做出这个决定。如上面代码所示,ASPX 代码隐藏没有检查 IsPostBack。它只是将值传递给 Presenter 以确定要采取的行动。
这可能会引出疑问:“但是,如果另一个用户控件作为视图导致了回发,会发生什么?”在这种情况下,当前时间将保留在标签的视图状态中,并在回发后再次显示。这可能可以接受,具体取决于客户端的需求。通常,这是一个对任何 Presenter 都要问的好问题:来自另一个用户控件的回发会对视图产生什么影响?事实上,即使你不使用 MVP,这也是一个值得问的好问题。有些操作应该始终发生,无论 IsPostBack 如何,而其他初始化步骤可能会被跳过。视图状态设置显然也对这个决定有很大影响。
当不是 IsPostBack 时,如图所示,表示器然后通过其接口设置视图的 CurrentTime
。序列图纯粹主义者可能会提出观点,即图表暗示正在发送两条消息——一条从 CurrentTimePresenter 到 ICurrentTimeView,然后一条从 ICurrentTimeView 到 CurrentTimeView.ascx——而实际上只有一条从 CurrentTimePresenter 到 CurrentTimeView.ascx,以多态方式发送。包含接口“中间人”是为了强调表示器不直接依赖于具体的视图。
D) IsPostBack 后的 Presenter InitView
在前面的步骤中,用户发出了 HTTP 请求,表示器在视图上设置了当前时间,并且 HTTP 响应已传递给用户。现在,用户单击“添加天数”按钮,这会导致回发。在 Presenter 上调用 InitView
之前,一切都像以前一样发生。此时,Presenter 检查 IsPostBack 并且不在视图上设置 CurrentTime
。
E) 用户控件处理按钮单击
ASPX 页面加载完成后,OnClick 事件会发送到用户控件。视图不应自行处理事件;它应立即将事件传递给表示器进行处理。通过查看用户控件的代码隐藏,您可以看到它确保已给定一个有效的表示器——更多的“契约式设计”——然后将命令传递给表示器。表示器随后验证页面是否有效,并相应地设置时间或错误消息。
上述是对带有事件处理的完整 MVP 周期的详尽分析。一旦掌握了 MVP,将所有组件就位所需的时间就非常少。记住始终从单元测试开始,并让单元测试驱动开发。单元测试不仅有助于确保 MVP 组件正常工作,它们还充当定义组件之间通信协议的点。附录 B 中可以找到 MVP 单元测试的 Visual Studio 代码片段。我们现在将研究页面重定向的处理。
三、使用 MVP 和 PageMethods 的页面重定向
在开发企业应用程序时,应用程序流始终是一个问题。谁来负责页面重定向?操作重定向是否应该存储在可配置的 XML 文件中?第三方工具(如 Maverick.NET 或 Spring.NET)是否应该处理页面流?就我个人而言,我喜欢将页面重定向尽可能地靠近操作。换句话说,我觉得将操作/重定向存储在外部 XML 文件中会导致进一步的间接性,这可能难以理解和维护。好像我们还没有足够担心的事情!另一方面,ASPX 代码隐藏中的硬编码重定向是脆弱的,解析繁琐,并且不是强类型的。为了解决这个问题,免费的 下载 PageMethods 允许您拥有强类型重定向。因此,不必编写 Response.Redirect("../Project/ShowProjectSummary?projectId=" + projectId.ToString() + "&userId=" + userId.ToString())
,PageMethods 提供了一个强类型重定向,看起来更像 Response.Redirect(MyPageMethods.ShowProjectSummary.ShowSummaryFor(projectId, userId))
。重定向是强类型的,因此在编译时进行检查。
关于页面重定向的 MVP 相关问题仍然存在:谁应该负责重定向,以及应该如何启动重定向?我相信这个问题有许多有效的答案,但我将提出一个我发现相当成功的解决方案。为每个可能的输出向表示器添加一个事件。例如,假设一个网站由两个页面组成。第一个页面列出了一些项目;第二个页面,通过单击其中一个项目名称旁边的“编辑”按钮到达,允许用户更新项目名称。更新项目名称后,用户应再次重定向到项目列表页面。为了实现这一点,表示器应引发一个事件,表明项目名称已成功更改,然后视图初始化器(ASPX 页面)应执行适当的重定向。请注意,以下内容是说明性的,与迄今为止讨论的“当前时间”示例无关。
表示器
...
public event EventHandler ProjectUpdated;
public void UpdateProjectNameWith(string newName)
{
...
if (everythingWentSuccessfully)
{
ProjectUpdated(this, null);
}
else
{
view.Message = "That name already exists. Please provide a new one!";
}
}
...
ASPX 代码隐藏
...
protected void Page_Load(object sender, EventArgs e)
{
EditProjectPresenter presenter =
new EditProjectPresenter(editProjectView);
presenter.ProjectUpdated += new EventHandler(HandleProjectUpdated);
presenter.InitView();
}
private void HandleProjectUpdated(object sender, EventArgs e)
{
Response.Redirect(
MyPageMethods.ShowProjectSummary.Show(projectId, userId));
}
...
采用这种方法可以将页面重定向排除在表示器和视图之外。根据经验法则,表示器永远不应该需要引用 System.Web
。此外,将重定向与视图(即用户控件)分离,并允许视图再次被其他视图初始化器(即其他 ASPX 页面)使用。同时,它将应用程序流留给每个单独的视图初始化器。这是使用基于事件的重定向模型和用户控件作为视图 MVP 的最大好处。
四、MVP 的呈现安全性
通常,一列、一个按钮、一个表格或任何其他元素应根据查看网站的用户的权限进行显示/隐藏。同样,当视图包含在一个视图初始化器中而不是包含在不同的视图初始化器中时,某个项目可能会被隐藏。安全性应由表示器决定,但视图应处理如何实现该决定。回到“当前时间”示例,假设客户端只希望“添加天数”部分在偶数天(例如 2、4、6)对用户可用。客户端喜欢让用户猜!视图可以将此区域封装在一个面板中,如下所示
...
<asp:Panel id="pnlAddDays" runat="server" visible="false">
<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" />
</asp:Panel>
...
请注意,面板的可见性被悲观地设置为 false
。尽管在这种情况下差别不大,但对于显示安全元素,悲观一些比乐观更好。视图的代码隐藏随后会公开一个 setter 来显示/隐藏面板
...
public bool EnableAddDaysCapabilities
{
set { pnlAddDays.Visible = value; }
}
...
请注意,视图不直接公开面板。这是有意为之,原因有二:1) 直接公开面板将要求表示器引用 System.Web
,这是我们希望避免的,2) 公开面板将表示器与视图的“实现细节”绑定。表示器与视图实现方式的绑定越紧密,其与其他视图重用的可能性就越小。与其他的 OOP 场景一样,公开视图实现细节的优缺点需要与表示器更松散的耦合进行权衡。
最后,在 InitView 期间,Presenter 检查用户是否应该被允许使用“添加天数”功能,并相应地设置视图上的权限
...
public void InitView()
{
view.EnableAddDaysCapabilities = (DateTime.Now.Day % 2 == 0);
}
...
这个简单的例子可以扩展到各种场景,包括安全检查。请注意,这不能替代内置的 .NET 安全机制,但它用于增强 .NET 安全机制以实现更精细的控制。
五、MVP 应用程序架构
终于!所有这些如何在数据驱动的企业应用程序中协同工作?本例中的“企业应用程序”是指具有逻辑分离层次的应用程序,包括表示层、域层和数据访问层。下图显示了完全架构解决方案的概述,后跟讨论。
每个凸起的方框代表应用程序的一个独特专业化。每个灰色方框代表一个独立的物理程序集,例如 MyProject.Web.dll、MyProject.Presenters.dll、MyProject.Core.dll。箭头代表依赖关系。例如,.Web 程序集依赖于 .Presenters 和 .Core 程序集。这些程序集通过依赖反转和依赖注入技术避免了双向依赖。我首选的依赖注入方式——上图中标记为“DI”——注入到视图初始化器中是通过 Castle Windsor 项目。数据层然后使用 ORM 框架 NHibernate 与数据库通信。
有关依赖注入的入门知识,请阅读 CodeProject 文章“依赖注入实现松散耦合”。此外,有关此架构的完整概述(不包括 .Presenters 层和 Castle Windsor 集成),请阅读 CodeProject 文章“ASP.NET 的 NHibernate 最佳实践”。本文还介绍了如何设置和运行示例应用程序。是的,这些都是我写的其他文章的无耻广告,但这两篇都是充分理解示例解决方案的必读内容。如有任何关于架构的问题,请随时提出。
总结
乍一看,实现 MVP 似乎需要大量额外工作。事实上,在开发的初期阶段,它会稍微减慢开发速度。然而,在企业应用程序开发的所有阶段使用它之后,这种方法的长期好处远远超过了最初对该模式的不适感。MVP 将极大地扩展您的单元测试能力,并在项目的整个生命周期中(尤其是在维护阶段)保持代码的可维护性。说到底,我并不是建议您在所有企业级 ASP.NET 项目中使用 MVP,而只是在您希望能够正常工作的项目中使用!;) 严肃地说,MVP 并非适用于所有情况。应用程序的架构应该适合手头的任务,并且只有在有必要时才应该增加复杂性。显然,MVP 和用户控件作为视图的 MVP 只是众多架构选项中的两个。然而,如果使用得当,MVP 可以让您对您的演示逻辑充满信心,因为它可以使大部分原本在代码隐藏中的代码变得可测试和可维护。
附录 A:其他参考文献
附录 B:Visual Studio 2005 的 MVP 单元测试代码片段
如果使用默认的 VS 2005 安装位置,请将以下内容复制到“C:\Program Files\Microsoft Visual Studio 8\VC#\Snippets\1033\Visual C#”下的“MVP Test Init.snippet”文件中。
<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets
xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>MVP Test Init</Title>
<Shortcut>mvpTestInit</Shortcut>
<Description>Code snippet for creating an initial
unit test for a new MVP setup.</Description>
<Author>Billy McCafferty</Author>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>viewInterface</ID>
<ToolTip>Name of the view interface</ToolTip>
<Default>IView</Default>
</Literal>
<Literal>
<ID>presenter</ID>
<ToolTip>Name of the presenter class</ToolTip>
<Default>Presenter</Default>
</Literal>
<Literal>
<ID>mockView</ID>
<ToolTip>Name of the mock view
used in the unit test</ToolTip>
<Default>MockView</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[ [Test]
public void TestInitView()
{
$viewInterface$ view = new $mockView$();
$presenter$ presenter = new $presenter$(view);
view.AttachPresenter(presenter);
presenter.InitView();
}
private class $mockView$ : $viewInterface$
{
public void AttachPresenter($presenter$ presenter)
{
}
}
$end$]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
历史
- 2006.07.02 - 初次发布
- 2007.04.18 - 在文章顶部添加了过去一年学到的经验
- 2007.07.10 - 文章编辑并移至 CodeProject.com 主文章库