作者:Billy McCafferty 翻译:张善友<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
原文地址: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”之后发生了什么.。
<?xml:namespace prefix = v ns = "urn:schemas-microsoft-com:vml" />
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,也是一个好问题。
When not IsPostBack, the Presenter then sets the <CODE>CurrentTime</CODE> of the view
via its interface. (Sequence diagram purists may raise the point that the
diagram implies two messages are being sent - one from CurrentTimePresenter to
ICurrentTimeView and then one from ICurrentTimeView to CurrentTimeView.ascx -
when in fact only one is being sent from CurrentTimePresenter to
CurrentTimeView.ascx, polymorphically. The interface "middleman" is included to
emphasize that the Presenter does not depend on the concrete View directly.)
D) Presenter InitView after IsPostBack
In the preceding steps, the user made the HTTP request, the Presenter set the
current time on the View, and the HTTP response was delivered to the user. Now,
the user clicks the "Add Days" button which causes a post-back. Everything
occurs as before until <CODE>InitView</CODE> is called on the Presenter. At this
point, the Presenter tests for IsPostBack and does <I>not</I> set the
<CODE>CurrentTime</CODE> on the View.
E) Button Click Handled by User Control
After the Page_Load of the ASPX page has occurred, the OnClick event is then
raised to the user control. The View should not handle the event itself; it
should immediately pass the event on to the Presenter for action. By looking at
the <A href="#EventPasser">code-behind of the user control</A>, you can see that
it makes sure it has been given a valid presenter (more "Design by Contract")
and then hands the command off to the Presenter. then verifies that the page was
valid and sets the time or error message, accordingly.
The above has been an exhaustive analysis of a complete MVP cycle with event
handling. Once you get the hang of MVP, it takes very little time to get all the
pieces in place. Remember to always begin with a unit test and let the unit
tests drive the development. The unit tests not only help ensure that the MVP
pieces are working correctly, they also serve as the point for defining the
communications protocol among the pieces. (A Visual Studio code snippet for an
MVP unit test can be found in We'll now
take a look at look at handling page redirection.
III. Page Redirects with MVP & PageMethods
In developing enterprise application, application flow is always a concern.
Who's going to take care of page redirects? Should action redirects be stored in
a configurable XML file? Should a third party tool such as <A
href="http://mavnet.sourceforge.net/" target=_blank>Maverick.NET</A> or <A
href="http://www.springframework.net/" target=_blank>Spring.NET</A> handle page
flow? Personally, I like to keep the page redirects as close to the action as
possible. In other words, I feel that storing action/redirects in an external
XML file leads to further indirection that can be tedious to understand and
maintain. (As if we don't have enough to worry about already!) On the other
hand, hard-coded redirects in the ASPX code-behind are fragile, tedious to parse
and not strongly typed. To solve this problem, PageMethods, free for download at
<A href="http://metasapiens.com/PageMethods"
target=_blank>http://metasapiens.com/PageMethods</A>, allows you to have
strongly typed redirects. So instead of writing <CODE>Response.Redirect("../Project/ShowProjectSummary?projectId=" +
projectId.ToString() + "&userId=" +
userId.ToString())</CODE>, PageMethods provides a strongly typed redirect that
would look more like
<CODE>Response.Redirect(MyPageMethods.ShowProjectSummary.ShowSummaryFor(projectId,
userId))</CODE>. The redirect is strongly typed and, therefore, checked at
compile time. </P>
<P>An MVP related question concerning page redirects remains: who should be
responsible for making a redirect and how should the redirect be initiated? (I
believe there are a number of valid answers to this question but will propose a
solution that I've found to be rather successful.) Add one event to the
Presenter for each outcome that is possible. For example, assume a website is
made up of two pages. The first page lists a number of projects; the second
page, reached by clicking "Edit" next to one of the project names, allows the
user to update the project's name. After updating the project name, the user
should be redirected to the project listing page again. To implement this, the
Presenter should raise an event showing that the project name was successfully
changed and then the View Initializer, the ASPX page, should execute the
appropriate redirect. (Note that the following is illustrative and not
associated with the "current time" example discussed thus far.)
<H4>Presenter:</H4><PRE>...
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 Code-Behind...
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));
}
...
</PRE>Taking this approach keeps page redirection out of the Presenter and out
of the View. As a rule of thumb, the Presenter should never require a reference
to <CODE>System.Web</CODE>. Furthermore, disassociating redirects from the View
- the user control - allows the View to be used again by other View Initializers
- other ASPX pages - while leaving application flow up to each individual View
Initializer. This is the greatest benefit of using an event based model of
redirection with User Control-as-View MVP.
IV. Presentation Security with MVP
Often times, a column, button, table or whatever should be shown/hidden based
on the permissions of the user viewing the website. Likewise, an item may be
hidden when a View is included in one View Initializer vs. being included in
different View Initializer. The security should be determined by the Presenter
but the View should handle how that decision should be implemented. Picking up
again with the "current time" example, assume that the client only wants the
"Add Days" section to be available for users on even days; e.g. 2, 4, 6, etc.
(The client likes to keep the users guessing!) The View could encapsulate this
area within a panel, as follows:
...
<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>
...
Note that the panel's visibility is pessimistically set to <CODE>false</CODE>. Although it would not make much
difference in this case, it is better to be pessimistic about showing secure
elements than the other way around. The code-behind of the View would then
expose a setter to show/hide the panel: ...
public bool EnableAddDaysCapabilities {
set { pnlAddDays.Visible = value; }
}
...
Note that the View does not expose the panel directly. This is intentionally
done for two reasons: 1) exposing the panel directly would require that the
Presenter have a reference to <CODE>System.Web</CODE> - something we want to
avoid, and 2) exposing the panel ties the Presenter to an "implementation
detail" of the View. The more a Presenter is tied to how a View is implemented,
the less likely it will be reusable with other Views. As with other OOP
scenarios, the pros and cons of exposing implementation details of the View need
to be weighed against looser coupling to the Presenter.
Finally, during InitView, the Presenter checks if the user should be allowed
to use the add-days functionality and sets the permission on the View,
accordingly:
public void InitView() {
view.EnableAddDaysCapabilities = (DateTime.Now.Day % 2 == 0);
}
...
This simple example can be extended to a varied number of scenarios
including security checks. Note that this is not a replacement for built-in .NET
security, but it serves to augment it for finer control.
V. Application Architecture with MVP
Finally! How does all of this fit together in a data-driven, enterprise
application? "Enterprise application," in this instance, is an application that
has logically separated tiers including presentation, domain and data-access
layers. The following graph shows an overview of a fully architected solution
with discussion following.
each raised box represents a distinct specialization of the application. Each
gray box then represents a separate, physical assembly; e.g. MyProject.Web.dll,
MyProject.Presenters.dll, MyProject.Core.dll, etc. The arrows represent
dependencies. For example, the <I>.Web</I> assembly depends on the
<I>.Presenters</I> and <I>.Core</I> assemblies. The assemblies avoid
bi-directional dependency using the techniques Dependency Inversion and
Dependency Injection. My preferred means of Dependency Injection ("DI" in the
above graph) to the View Initializers is via the <A
href="http://www.castleproject.org/index.php/Windsor_Container"
target=_blank>Castle Windsor</A> project. The data layer then uses the ORM
framework, <A href="http://www.hibernate.org/343.html"
target=_blank>NHibernate</A>, for communicating with the database. </P>
<P>For a primer on Dependency Injection, read the CodeProject article entitled
"Dependency Injection for Loose Coupling" at <A
href="http://www.codeproject.com/csharp/DependencyInjection.asp"
target=_blank>http://www.codeproject.com/csharp/DependencyInjection.asp</A>.
Additionally, for a complete overview of this architecture, sans the
<I>.Presenters</I> layer and Castle Windsor integration, read the CodeProject
article entitled "NHibernate Best Practices with ASP.NET" at <A
href="http://www.codeproject.com/aspnet/NHibernateBestPractices.asp">http://www.codeproject.com/aspnet/NHibernateBestPractices.asp</A>.
This article also describes how to setup and run the sample application. (Yes,
these are both <I>shameless</I> plugs for other articles I have written - but
both required reading to fully appreciate the sample solution.) Please feel free
to raise any questions concerning the architecture.
In Summary
At first glance, implementing MVP looks like a lot of extra work. In fact, it
will slow development a bit during the <I>initial</I> stages of
development. But after using it in all stages of enterprise application
development, the long-term benefits of using the approach far outweigh the
initial feelings of discomfort with the pattern. MVP will greatly extend your
ability to unit test and keep code more maintainable throughout the lifetime of
the project - especially during the maintenance phase. When it comes right down
to it, I'm not suggesting that you use MVP on all your enterprise ASP.NET
projects, just the projects that you want to work! ;) In all seriousness,
MVP is not appropriate in all situations. An application's architecture
should fit the task at hand and complexity should not be added unless
warrented. Obviously, MVP and User-Control-as-View MVP are
just two architectural options among many. But, if used
appropriately, MVP allows you to be confident in your presentation logic by
making most of the code that would have been in a code-behind, testable and
maintainable.
Appendix A: Additional References
Martin Fowler's soon-to-be-published overview of MVP:
The humble dialog box:
.
<LI>MVC vs. MVP: <A
href="http://www.darronschall.com/weblog/archives/000113.cfm"
target=_blank>http://www.darronschall.com/weblog/archives/000113.cfm</A>.
<LI>A thank you goes out to Jeffrey Palermo for inspiring the above
architectural diagram at <A
href="http://codebetter.com/blogs/jeffrey.palermo/archive/<?xml:namespace prefix = st1 ns = "urn:schemas-microsoft-com:office:smarttags" />2005/04/02/128598.aspx"
target=_blank>http://codebetter.com/blogs/jeffrey.palermo/archive/2005/04/02/128598.aspx</A>. </LI></UL>
<P></P>
<H2><A name=AppendixB>Appendix B: MVP Unit Test Code-Snippet for Visual Studio
2005
With a default VS 2005 install location, copy the following contents to "MVP
Test Init.snippet" under "C:\Program Files\Microsoft Visual Studio
8\VC#\Snippets\1033\Visual C#." <PRE><?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>