引用
http://www.codeproject.com/aspnet/ModelViewPresenter.asp
Author's note added October 15, 2007 - Suggestion of MVC over MVP
As we progress as developers, we strive to seek out the "best" way to perform our craft. The chosen methods to attain this lofty goal always bring with them a number of developmental trade-offs. Some techniques may simplify the code but lessen fine grained control while others enable greater power while introducing complexity. Model-View-Presenter with ASP.NET is a wonderful example of the latter. MVP aims to facilitate test-driven development of ASP.NET applications with the unfortunate introduction of added complexity. So while developers will be more confient in the quality of their product, MVP hinders their ability to easily maintain code. Yes, the tests will (hopefully) inform the developer that a bug has been introduced, but the inherent complexity of MVP makes it difficult for later team members to become comfortable with the code base and maintain it as project development continues.
Fortunately, as time progresses quickly in our field, resources and tools become available which enhance our ability to write powerful applications while simplifying the coding process itself. The introduction of NHibernate, for example, eliminated vast amounts of data-access code while still providing powerful facilities for managing transactions and dynamically querying data. Castle MonoRail (and Microsoft's upcoming spin-off of this framework) now does for writing testable and maintainable .NET web applications what NHibernate (and the upcoming LINQ to Entities) did for ADO.NET. This is not to say that the previous techniques were necessarily wrong, but that they were only applicable considering the developer's toolset that was available at the time of selection.
In adapting to the evolution of our field, it is important for developers to note when an accepted technique is no longer valuable in light of current alternatives. Specifically, MVP was a very powerful technique for writing ground-up, test-driven ASP.NET applications but is no longer a strong candidate for consideration when compared to the time saving benefits and simplicity of Castle MonoRail and Microsoft's upcoming MVC framework. Oddly, it is sometimes difficult to "give up" on something that worked perfectly fine before, but that's the nature of our business ... one tenet that's not likely to change anytime soon.
Of this article, I believe it to be of continued value to those maintaining legacy applications built upon MVP and for those interested in learning a solid domain driven architecture which is discussed further below and in detail in another post.
In summary, although I still believe that MVP is the best technique for developing ground-up ASP.NET solutions, I believe that there are off-the-shelf frameworks which make the entire job a heck of a lot simpler.
Introduction
After years of maintaining thousands of lines of ASP spaghetti code, Microsoft has finally given us a first class web development platform: ASP.NET. ASP.NET instantly brought a basic separation of concerns between presentation and business logic by introducing the code-behind page. Although introduced with good intentions and perfect for basic applications, the code-behind still lacks in a number of aspects when developing enterprise web applications:
- The code-behind invites melding the layers of presentation, business logic and data-access code. This occurs because the code-behind page often serves the role of an event handler, a workflow controller, a mediator between presentation and business rules, and a mediator between presentation and data-access code. Giving the code-behind this many responsibilities often leads to unmanageable code. In an enterprise application, a principle of good design is to maintain proper separation of concerns among the tiers and to keep the code-behind as clean as possible. With Model-View-Presenter, we'll see that the code-behind is greatly simplified and kept strictly to managing presentation details.
- Another drawback to the code-behind model is that it is difficult to reuse presentation logic between code-behind pages without enlisting helper/utility classes that consolidate the duplicated code. Obviously, there are times that this provides an adequate solution. However, it often leads to incohesive classes that act more like ASP includes than first class objects. With proper design, every class should be cohesive and have a clear purpose. A class named ContainsDuplicatePresentationCodeBetweenThisAndThat.cs usually doesn't qualify.
- Finally, it becomes nearly prohibitive to properly unit test code-behind pages as they are inseparably bound to the presentation. Options such as NUnitAsp may be used, but they are time consuming to implement and difficult to maintain. They also slow down unit-test performance considerably, where unit tests should always be blazingly fast.
Various techniques may be employed to promote a better separation of concerns from the code-behind pages. For example, the Castle MonoRail project attempts to emulate some of the benefits of Ruby-On-Rails but abandons the ASP.NET event model in the process. Maverick.NET is a framework that optionally supports the ASP.NET event model but leaves the code-behind as the controller in the process. Ideally, a solution should be employed that leverages the ASP.NET event model while still allowing the code-behind to be as simple as possible. The Model-View-Presenter pattern does just that without relying on a third party framework to facilitate this goal.
Model-View-Presenter
Model-View-Presenter (MVP) is a variation of the Model-View-Controller (MVC) pattern but specifically geared towards a page event model such as ASP.NET. For a bit of history, MVP was originally used as the framework of choice behind Dolphin Smalltalk. The primary differentiator of MVP is that the Presenter implements an Observer design of MVC but the basic ideas of MVC remain the same: the model stores the data, the view shows a representation of the model, and the presenter coordinates communications between the layers. MVP implements an Observer approach with respect to the fact that the Presenter interprets events and performs logic necessary to map those events to the proper commands to manipulate the model. For more reading on MVC vs. MVP, take a look at Darron Schall's concise entry on the subject. What follows is a detailed examination of MVP in the form of three example projects.
Author's note: Martin Fowler has suggested that MVP be split between two "new" patterns called Supervising Controller and Passive View. Go here for a very short synopsis of the split. The content described herein is more consistent with Supervising Controller as the View is aware of the Model.
A most trivial example
In this example project, the client wants a page that shows the current time. Thank goodness they started us off with something easy! The ASPX page that will show the time is the "View." The "Presenter" is responsible for determining the current time -- i.e. the "Model" -- and giving the Model to the View. As always, we start with a unit test:
Collapse
[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; }
get { return currentTime; }
}
private DateTime currentTime = DateTime.MinValue;
}
}
The above unit test, along with the diagram, describes the elements of the MVP relationship. The very first line creates an instance of MockCurrentTimeView
. As seen with this unit test, all of the Presenter logic can be unit tested without having an ASPX page, i.e. the View. All that is needed is an object that implements the View interface; accordingly, a mock view is created that stands in place of the "real" view.
The next line creates an instance of the Presenter, passing an object that implements ICurrentTimeView
via its constructor. In this way, the Presenter can now manipulate the View. As seen in the diagram, the Presenter only talks to a View interface. It does not work with a concrete implementation directly. This allows multiple Views, implementing the same View interface, to be used by the same Presenter.
Finally, the Presenter is asked to InitView()
. This method will get the current time and pass it to the View via a public property exposed by ICurrentTimeView
. A unit-test assertion is then made that the CurrentTime
on the view should now be greater than its initial value. A more detailed assertion could certainly be made if needed.
All that needs to be done now is to get the unit test to compile and pass!
ICurrentTimeView.cs: the View interface
As a first step towards getting the unit test to compile, ICurrentTimeView.cs should be created. This View interface will provide the conduit of communication between the Presenter and the View. In the situation at hand, the View interface needs to expose a public property that the Presenter can use to pass the current time, the Model, to the View.
public interface ICurrentTimeView
{
DateTime CurrentTime { set; }
}
The View only needs a setter for the current time since it just needs to show the Model, but providing a getter allows CurrentTime
to be checked within the unit test. So instead of adding a getter to the interface, it can be added to MockCurrentTimeView
and need not be defined in the interface at all. In this way, the exposed properties of the View can be unit tested without forcing extraneous setters/getters to be defined in the View Interface. The described unit test above shows this technique.
CurrentTimePresenter.cs: the Presenter
The presenter will handle the logic of communicating with the Model and passing Model values to the View. The Presenter, needed to make the unit test compile and pass, is as follows.
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;
}
Once the above items have been developed -- the unit test, the mock view, the view and the presenter -- the unit test will now compile and pass successfully. The next step is creating an ASPX page to act as the real View. As a quick side, take note of the ArgumentNullException
check. This is a technique known as "Design by Contract." Putting checks like this all over your code will greatly cut down on tracking down bugs. For more information about Design by Contract, see this article and this article.
ShowMeTheTime.aspx: the View
The actual View needs to do the following:
- The ASPX page needs to provide a means for displaying the current time. As shown below, a simple label will be used for display.
- The code-behind must implement
ICurrentTimeView
.
- The code-behind needs to create the Presenter, passing itself to the Presenter's constructor.
- After creating the Presenter,
InitView()
needs to be called to complete the MVP cycle.
The ASPX page
...
<asp:Label id="lblCurrentTime" runat="server" />
...
The ASPX code-behind page
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(); }
}
}
Is that it?
In a word, yes. But there is much more to the story! A drawback to the above example is that MVP seems like a lot of work for such little gain. We've gone from having one ASPX page to having a Presenter class, a View interface and a unit testing class. The gain has been the ability to unit test the Presenter, i.e. the ability to conveniently unit test code that would normally be found in the code-behind page. As is the case with trivial examples, the advantages of MVP shine when developing and maintaining enterprise web applications, not when writing "hello world"-like samples. The following topics elaborate the usage of MVP within an enterprise, ASP.NET application.
MVP within enterprise ASP.NET applications
In the previous, simple example, the ASPX page itself acted as the View. Treating the ASPX in this way was sufficient in that the page had only one simple purpose - to show the current time. But in more representative projects, it is often the case that a single page will have one or more sections of functionality whether they be WebParts, user controls, etc. In these more typical of enterprise applications, it is important to keep functionality logically separated and to make it easy to move/replicate functionality from one area to another. With MVP, user controls can be used to encapsulate Views while the ASPX pages act as "View Initializers" and page redirectors. Extending the previous example, we need only modify the ASPX page to implement the change. This is another benefit of MVP; many changes can be made to the View layer without having to modify the Presenter and Model layers.
ShowMeTheTime.aspx redux: the View initializer
With this new approach, using user controls as the view, ShowMeTheTime.aspx is now responsible for the following:
- The ASPX page needs to declare the user control which will implement
ICurrentTimeView
.
- The ASPX code-behind needs to create the Presenter, passing the user control to the Presenter's constructor.
- After giving the View to the Presenter, the ASPX needs to call
InitView()
to complete the MVP cycle.
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
{
protected void Page_Load(object sender, EventArgs e)
{
InitCurrentTimeView();
}
private void InitCurrentTimeView()
{
CurrentTimePresenter presenter =
new CurrentTimePresenter(currentTimeView);
presenter.InitView();
}
}
CurrentTimeView.ascx: the user control-as-view
The user control now represents the bare-bones View. It is as "dumb" as it can be - which is exactly how we want a View to be.
The ASCX page
...
<asp:Label id="lblCurrentTime" runat="server" />
...
The ASCX code-behind page
public partial class Views_CurrentTimeView : UserControl, ICurrentTimeView
{
public DateTime CurrentTime
{
set { lblCurrentTime.Text = value.ToString(); }
}
}
Pros and cons of the user Control-as-View approach
Obviously, the primary drawback to the User Control-as-View approach to MVP is that it adds yet another piece to the equation. The entire MVP relationship is now made up of: unit test, presenter, view interface, view implementation (the user control) and the view initializer (the ASPX page). Adding this additional layer of indirection adds to the overall complexity of the design. The benefits of the user control-as-View approach include:
- The View can be easily moved from one ASPX page to another. This happens regularly in a mid to large sized web application.
- The View can be easily reused by different ASPX pages without duplicating much code at all.
- The View can be initialized differently by different ASPX pages. For example, a user control could be written that displays a listing of projects. From the reporting section of the site, the user may view and filter all the projects available. From another section of the site, the user may only view a subset of the projects and not have the ability to run filters. In implementation, the same View can be passed to the same presenter, but then each ASPX page, in its respective section of the site, would call a different method on the Presenter to initialize the View in a unique way.
- Additional Views can be added to the ASPX page without adding much additional, coding overhead. Simply include the new user control-as-view into the ASPX page and link it to its Presenter in the code-behind. Placing multiple sections of functionality within the same ASPX page, without using user controls, quickly creates a maintenance headache.
The previous example described, essentially, a one-way round of communications between a Presenter and its View. The Presenter communicated with the Model and delivered it to the View. In most situations, events occur which need to be handed off to the Presenter for action. Furthermore, some events depend on whether or not a form was valid and whether or not IsPostBack had occurred. For example, there are some actions, such as data-binding, that may not be done when IsPostBack.
Disclaimer: Page.IsPostBack and Page.IsValid are web specific keywords. Therefore the following may make the presenter layer, as described, slightly invalid in non-web environments. However, with minor modifications it will work fine for WebForms, WinForms or mobile applications. In any case, the theory is the same but I welcome suggestions for making the presenter layer transferable to any .NET environment.
A simple event handling sequence
Continuing with the earlier example, assume that requirements now dictate that the user may enter a number of days to be added to the current time. The time shown in the View should then be updated to show the current time plus the number of days supplied by the user, assuming the user provided valid inputs. When not IsPostBack, the current time should be displayed. When IsPostBack, the Presenter should respond to the event accordingly. The sequence diagram below shows what occurs upon the user's initial request (top half of diagram) and what happens when the user clicks the "Add Days" button (bottom half of diagram). A more thorough review of the sequence follows the diagram.
A) User Control-as-View created
This step simply represents the inline user control declaration found in the ASPX page. During page initialization, the user control gets created. It's included on the diagram to emphasize the fact that the user control implements ICurrentTimeView
. During Page_Load, the ASPX code-behind then creates an instance of the Presenter, passing the User Control-as-View via its constructor. So far, everything looks identical to what was described in the section "Encapsulating Views with User Controls."
B) Presenter attached to View
In order for an event to be passed from the user control, the View, to the Presenter, it must have a reference to an instance of CurrentTimePresenter
. To do this, the View Initializer, ShowMeTheTime.aspx, passes the Presenter to the View for later use. Contrary to initial reaction, this does not cause a bi-directional dependency between the Presenter and the View. Instead, the Presenter depends on the View interface and the View implementation depends on the Presenter to pass events off to. To see how it all works, let's take a step back to look at how all the pieces are now implemented.
ICurrentTimeView.cs: the View interface
public interface ICurrentTimeView
{
DateTime CurrentTime { set; }
string Message { set; }
void AttachPresenter(CurrentTimePresenter presenter);
}
Collapse
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" />
...
Collapse
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
{
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
As defined in the requirements, the Presenter should only show the current time if not IsPostBack. The important action to note is that the Presenter should decide what to do according to IsPostBack. It should not be the job of the ASPX code-behind to make this decision. As seen in the code above, the ASPX code-behind does no check for IsPostBack. It simply passes the value to the Presenter to determine what action to take.
This may lead to the question, "But what happens if another user control-as-view caused the post-back to occur?" In the scenario at hand, the current time would remain in the view state of the label and be displayed again after post back. This may be OK depending on the needs of the client. In general, it's a good question to ask of any Presenter: what impact will a post-back from another user control have on the View? In fact, it's a good question to ask even if you're not using MVP. There may be actions that should always occur, regardless of IsPostBack, while other initialization steps may be bypassed. View state settings obviously have a large impact on this decision, as well.
When not IsPostBack, as shown in the diagram, the Presenter then sets the CurrentTime
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 InitView
is called on the Presenter. At this point, the Presenter tests for IsPostBack and does not set the CurrentTime
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 code-behind of the user control, 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. 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 Appendix B. We'll now take a look at look at handling page redirection.
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 Maverick.NET or Spring.NET 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, the free download PageMethods allows you to have strongly typed redirects. So instead of writing Response.Redirect("../Project/ShowProjectSummary?projectId=" + projectId.ToString() + "&userId=" + userId.ToString())
, PageMethods provides a strongly typed redirect that would look more like Response.Redirect(MyPageMethods.ShowProjectSummary.ShowSummaryFor(projectId, userId))
. The redirect is strongly typed and, therefore, checked at compile time.
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.
Presenter
...
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));
}
...
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 System.Web
. Furthermore, disassociating redirects from the View -- i.e. the user control -- and allows the View to be used again by other View Initializers, i.e. other ASPX pages. At the same time, it leaves 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.
Oftentimes, 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. 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 false
. 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 System.Web
, 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.
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. The arrows represent dependencies. For example, the .Web assembly depends on the .Presenters and .Core 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 Castle Windsor project. The data layer then uses the ORM framework, NHibernate, for communicating with the database.
For a primer on Dependency Injection, read the CodeProject article entitled "Dependency Injection for Loose Coupling." Additionally, for a complete overview of this architecture, sans the .Presenters layer and Castle Windsor integration, read the CodeProject article entitled "NHibernate Best Practices with ASP.NET." This article also describes how to set up and run the sample application. Yes, these are both shameless plugs for other articles I have written, but both are 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 initial stages of development. However, 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 warranted. Obviously, MVP and User-Control-as-View MVP are just two architectural options among many. However, 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
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#".
Collapse
<?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>
History
- 2006.07.02 - Initial posting
- 2007.04.18 - Added lessons learned over the past year to top of article
- 2007.07.10 - Article edited and moved to the main CodeProject.com article base