|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Author's note added October 15, 2007 - Suggestion of MVC over MVPAs 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. IntroductionAfter 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:
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-PresenterModel-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 exampleIn 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: [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;
}
}
The above unit test, along with the diagram, describes the elements of the MVP relationship. The very first line creates an instance of The next line creates an instance of the Presenter, passing an object that implements Finally, the Presenter is asked to All that needs to be done now is to get the unit test to compile and pass! ICurrentTimeView.cs: the View interfaceAs 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 CurrentTimePresenter.cs: the PresenterThe 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 ShowMeTheTime.aspx: the ViewThe actual View needs to do the following:
The ASPX page...
<asp:Label id="lblCurrentTime" runat="server" />
...
The ASPX code-behind pagepublic 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
I. Encapsulating Views with user controlsIn 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 initializerWith this new approach, using user controls as the view, ShowMeTheTime.aspx is now responsible for the following:
The ASPX page...
<%@ Register TagPrefix="mvpProject"
TagName="CurrentTimeView" Src="./Views/CurrentTimeView.ascx" %>
<mvpProject:CurrentTimeView id="currentTimeView" runat="server" />
...
The ASPX code-behind pagepublic 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: the user control-as-viewThe 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 pagepublic partial class Views_CurrentTimeView : UserControl, ICurrentTimeView
{
public DateTime CurrentTime
{
set { lblCurrentTime.Text = value.ToString(); }
}
}
Pros and cons of the user Control-as-View approachObviously, 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:
II. Event handling with MVPThe 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 sequenceContinuing 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 createdThis 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 B) Presenter attached to ViewIn 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 ICurrentTimeView.cs: the View interfacepublic interface ICurrentTimeView
{
DateTime CurrentTime { set; }
string Message { set; }
void AttachPresenter(CurrentTimePresenter presenter);
}
CurrentTimePresenter.cs: the Presenterpublic 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 ViewThe 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 pagepublic 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 initializerThe ASPX page...
<%@ Register TagPrefix="mvpProject"
TagName="CurrentTimeView" Src="./Views/CurrentTimeView.ascx" %>
<mvpProject:CurrentTimeView id="currentTimeView" runat="server" />
...
The ASPX code-behind pagepublic 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 InitViewAs 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 D) Presenter InitView after IsPostBackIn 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 E) Button click handled by user controlAfter 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. III. Page redirects with MVP & PageMethodsIn 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 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 IV. Presentation security with MVPOftentimes, 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 ...
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 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 MVPFinally! 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 summaryAt 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
Appendix B: MVP unit test code-snippet for Visual Studio 2005With 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#". <?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
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||