引用
http://www.codeproject.com/useritems/MVP_in_ASPNET.asp
Introduction
I have been using the model-view-presenter pattern in my ASP.NET web applications for a little over a year now. I was first introduced to MVP in a smart-client application using an event-driven implementation well before applying it in ASP.NET. After spending some time in a thick client environment, I found that applying MVP to the web reveals a new set of problems that need to be addressed. This article will describe these issues and provide an implementation for ASP.NET that I feel maximizes usability and testability.
What this article will do is briefly explain the basics of the pattern and provide three implementations of MVP for ASP.NET. I provide three implementations to allow the reader to see how the pattern can vary, and how the roles for the ASPX page, ASCX user control, and Presenter are defined in each. There is no one correct implementation of MVP in ASP.NET. Whichever implementation is used is really just a matter of personal preference and theoretical debate.
Model-View-Presenter in ASP.NET: Setting the Stage
By default, ASP.NET implements a Page Controller pattern that does not promote good separation of concerns nor testability. The dependency on the ASP.NET runtime makes testing of the implementation difficult without generating testing scenarios that are impractical. Since unit testing of view specific pages is difficult, we look for patterns that promote testability. MVP is such a pattern.
Model-view-presenter is a design pattern that aims to increase separation of concerns and testability. Its primary goal is to separate view specific logic from domain/business logic. As we design object-oriented applications, we desire objects that are loosely coupled and can be easily reused. In order to do so, we need to build classes and layers that are specific to certain tasks, such as view, presentation, service, and data access to name a few. In ASP.NET, it is too easy to add domain or business logic to our ASPX page or ASCX user control classes, creating tightly coupled classes that become difficult to reuse and test. MVP seeks to separate view-specific logic from domain/business logic by using a presentation layer.
The secondary goal of MVP is to improve testability of the view. It is difficult to write a unit test for a class that is dependent upon Session or ViewState, Ajax, Html or web controls, and domain/business objects. Instead, we leave that view-specific logic in the ASPX/ASCX classes and pull presentation and domain/business logic out of the view and put them in their appropriate classes. In MVP, the presenter acts as a mediator between the view and our domain/business logic.
Martin Fowler has split the MVP pattern into two new patterns, Supervising Controller and Passive View. Unlike a true MVC (model-view-controller) framework that enforces a strict separation of the view layer from the presentation (controller), in ASP.NET, this separation is not enforced by default. Because of this, it is difficult to enforce any one implementation of MVP without conscientious effort on the part of the developer, and the grey area between implementing a Supervising Controller and a Passive View widens. As a rule of thumb when creating my presenters, I try and pull as much logic as possible out of the view that I want under test, and put that into the presenter. I let the view handle view-specific logic such as JavaScript, HTML and WebControls, and Ajax frameworks. Since there is still some logic in my view, I tend to classify this as Supervising Controller versus Passive View, and after several sleepless nights debating this in my head, I am happy with Supervising Controller in ASP.NET.
If you need a more detailed introduction beyond what is mentioned above, you may find these links helpful.
GUI Architectures
Supervising Controller
Passive View
Presenter First
Model View Presenter with ASP.NET
ASP.NET Supervising Controller (Model View Presenter) From Schematic To Unit Tests to Code
Different Implementations of MVP in ASP.NET
While implementing MVP in ASP.NET, my designs have followed a few different schools of thought. One such approach was detailed by Billy McCafferty and another by Phil Haack. Since my introduction to MVP was event-driven in a windows application, I applied the event-driven approach that was most familiar to me. The stateless nature of the web was the first obstacle I found I needed to overcome. In ASP.NET, we recreate our MVP relationship with each trip to the server. Persisting state and referencing "Page.IsPostBack" becomes necessary. The sample application and the code snippets below illustrate how we recreate our presenter and pass the IsPostBack value to manage this difficulty. What I have discovered with the MVP pattern is that it can have many variations in ASP.NET, and choosing which one to implement is really a matter of preference. My preferred implementation contains characteristics of some of the authors mentioned above as well as what I have discovered for myself over the past year.
The next section will be divided up into three parts, one for each implementation. I will start with my original introduction to MVP in ASP.NET, then move on to my more familiar event-driven approach. Finally, I will provide a third implementation that I feel provides greater reusability. The sample application I am including with this article has basic examples of each implementation. Each section will describe which modules in the sample app correspond. The code snippets I am providing below are extremely simplistic and are not complete. They are thorough enough to illustrate their points.
The First Implementation
The first implementation is Billy McCafferty's. It introduces the role of the "view initializer and page redirector" to the ASPX page. The view is the ASCX user control, and the presenter only knows about the interface describing the view. The ASPX page is responsible for instantiating the presenter and passing it the view and any model objects the presenter requires. It then attaches the presenter to the view, so the view may reference the presenter when necessary. Lastly, it calls "InitView" on the presenter to simulate the Page.IsPostBack event in ASP.NET.
This example is implemented as the "Product" module in the sample application.
Collapse
Note: The code below is used to highlight the main points of this design. Please see the sample application for a working model.
The Presenter
public class Presenter
{
public Presenter(IView view, IModel model)
{
this.view = view;
this.model = model;
}
public void InitView(bool isPostBack)
{
if(!isPostBack)
{
view.SetProducts(model.GetProducts());
}
}
public void SaveProducts(IList<IProduct> products)
{
model.SaveProducts(products);
}
}
The ASPX Page: The Starting Point
The ASPX HTML references the ASCX user control, and in code behind we have this.
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
presenter = new Presenter(view,model;
view.AttachPresenter(presenter);
presenter.InitView(Page.IsPostBack);
}
The ASCX User Control
public void AttachPresenter(Presenter presenter)
{
this.presenter = presenter;
}
public void SetProducts(IList<IProduct> products)
{
}
The View Interface
public interface IView
{
void AttachPresenter(Presenter presenter);
void SetProducts(IList<IProduct> products);
}
The Second Implementation
The second implementation is an event-driven approach. It uses the "view initializer and page redirector" role for the ASPX page just as the first. The ASCX user control implements a view interface that declares events that will be raised to a presenter. The view knows nothing about the presenter; it only knows how to raise events. The ASPX page initializes the presenter, passing to it the view and any model objects. The ASPX page is not responsible for attaching the presenter to a view, nor calling "InitView" on the presenter. Its only job is to wire up the presenter with the view instance and model objects, and to respond to events that the presenter might raise, such as a page redirect or some type of status event.
This example is implemented as the "Customer" module in the sample application.
Collapse
Note: The code below is used to highlight the main points of this design. Please see the sample application for a working model.
The Presenter
public class Presenter
{
public Presenter(IView view, IModel model)
{
this.view = view;
this.model = model;
this.view.OnViewLoad += new EventHandler<SingleValueEventArgs<bool>>(OnViewLoadListener);
this.view.SaveProducts += new EventHandler<SingleValueEventArgs<IList<IProduct>>>(SaveProductListener);
}
private void OnViewLoadListener(object sender, SingleValueEventArgs<bool> isPostBack)
{
if (!isPostBack.Value)
{
view.SetProducts(model.GetProducts());
}
}
private void SaveProductListener(object sender, SingleValueEventArgs<IList<IProduct>> products)
{
model.SaveProducts(products.Value);
}
}
The ASPX Page: The Starting Point
The ASPX HTML references the ASCX user control, and in code behind we have this.
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
presenter = new Presenter(this,model);
}
The ASCX User Control
protected override void OnLoad(EventArgs e)
{
EventHandler eventHandler = OnViewLoad;
if (eventHandler != null)
{
eventHandler(this, new SingleValueEventArgs<bool>(Page.IsPostBack));
}
base.OnLoad(e);
}
public void SetProducts(IList<IProduct> products)
{
}
protected void btnSave_Click(object sender, EventArgs e)
{
OnSaveProducts(GetProducts());
}
public event EventHandler<SingleValueEventArgs<string>> SaveProducts;
public virtual void OnSaveProducts(IList<IProduct>> products)
{
EventHandler<SingleValueEventArgs<IList<IProduct>>> eventHandler = SaveProducts;
if (eventHandler != null)
{
eventHandler(this, new SingleValueEventArgs<IList<IProduct>>(products));
}
}
The View Interface
public interface IView
{
event EventHandler OnViewLoad;
event EventHandler<SingleValueEventArgs<IList<IProduct>>>SaveProducts;
void SetProducts(IList<IProduct> products);
}
The Third Implemenation
The third implementation delegates the responsibility of creating the presenter, passing in the view and model, and calling "InitView" on the presenter to the ASCX user control (view). The view has a reference to its presenter. The presenter only knows about an interface to the view. The ASPX page is used to add the user control to the page, nothing further. Since the ASPX's responsibilities from the first and second implementations now fall firmly within the responsibility of the ASCX user control, my views are easily reusable throughout my application. I can drag a user control onto a new page and with it comes its presenter, all ready to go out of the box.
This example is implemented as the "Employee" module in the sample application.
Collapse
Note: The code below is used to highlight the main points of this design. Please see the sample application for a working model.
The Presenter
public class Presenter
{
public Presenter(IView view, IModel model)
{
this.view = view;
this.model = model;
}
public void InitView(bool isPostBack)
{
if(!isPostBack)
{
view.SetProducts(model.GetProducts());
}
}
public void SaveProducts(IList<IProduct> products)
{
model.SaveProducts(products);
}
}
The ASPX Page
The ASPX HTML references the ASCX user control, nothing futher in code behind.
The ASCX User Control: The Starting Point
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
presenter = new Presenter(this,model);
presenter.InitView(Page.IsPostBack);
}
public void SetProducts(IList<IProduct> products)
{
}
The View Interface
public interface IView
{
void SetProducts(IList<IProduct> products);
}
Reflecting on the Implementations
With each of the implementations, there are characteristics I favor and those I have difficulty accepting. Again, since there is no true MVP framework for ASP.NET as this time, there are no constraints forcing me to adhere to one implementation. Determining your flavor of MVP really depends on the level of separation of concerns with which you are most comfortable, and to what degree you feel your classes should be testable.
I like the use of the ASPX page as the "view initializer and page redirector" with the first two implementations. I feel that this is a well suited responsibility for the ASPX page that should not reside with the view. The view, in my opinion, should only be concerned with view-specific responsibilities. Determining what defines a view-specific responsibility is debatable and something I feel like I have wrestled with too often.
In the second implementation, I prefer how the view is ignorant of the presenter. The view is decoupled from the presenter. It only raises events, and the first event it raises, "OnViewLoad," signifies the control's loading state and passes the Page's IsPostBack value. The presenter listens for events on the view interface and commands the view to do some action when responding. The ASPX page instantiates the presenter, passing in the view instance and the model. It can register for events on the presenter as it needs.
What I do not like about the first two implementations is that since the ASPX page is involved, reusing the ASCX user control requires more work. If I want to add a user control to a different ASPX page, I now need to instantiate my model-view-presenter relationship in the new ASPX page. This becomes cumbersome when I have nested MVP relationships, where one user control may contain another user control. This dependency can be removed if I assign the responsibility of instantiating the presenter with the view and model from the ASPX page to the ASCX user control (view). As a result, the view now has more responsibility, but it is now more reusable. This added responsibility is something I may not agree with philosophically, but it helps improve usability, and my classes are still testable.
While I like the idea that my view is decoupled from the presenter in the event-driven approach, there really is no need for this separation. Using events is not always intuitive and reliable, and writing unit tests for events requires a little extra effort. There is no guarantee that the presenter subscribes to all the appropriate events on the view.
After settling my philosophical debates and finally feeling comfortable with certain responsibilities of the ASPX page, ASCX user control, and presenter in ASP.NET, I have created this third implementation. This third implementation is similar to the first implementation but it omits the role of the ASPX "view initializer and page redirector." On the positive side, my view is more reusable across my application since it is more self reliant. On the negative side, my view now has the added responsibility of creating the presenter and responding to events the presenter may raise. Even though I may feel that certain responsibilities are crossing boundaries, I keep reminding myself that this is MVP in ASP.NET - this is not a true MVC framework that enforces that good separation of concerns like Monorail.
Conclusion
MVP provides a number of advantages, but to me, the two most important are separation of concerns and testability. There is a fair amount of overhead involved in using MVP, so if you are not planning on writing unit tests, I would definitely reconsider using the pattern.
As we see with the three different implementations, there are numerous ways to implement the pattern in ASP.NET. There are even more ways than what I have chosen to display. Choose an implementation that best suits your needs. I have to work hard at implementing MVP in ASP.NET, and there are certain tradeoffs I need to be willing to accept. As long as my code is testable, reusable, maintainable, and there exists a good degree of separation of concerns, I am happy.
With Microsoft's news of releasing an MVC framework for ASP.NET, there is hope on the horizon for a framework that enforces good separation of concerns and testability. The Castle Project's Monorail is another MVC framework that I highly recommend. If you cannot wait for Microsoft's MVC framework, or do not wish to port your application to Monorail at this time, then implementing MVP could be your answer.
About the Sample Project
The sample project is written in ASP.NET 2.0 using C#. I am using the Northwind database. I am using SubSonic as my data access layer. Since SubSonic is built using the active record pattern, I do have to use interfaces in order to make my DAO classes testable. For my unit testing, I am using RhinoMocks as my mocking framework.
The sample application is comprised of five projects. The WebApp, Model, Presentation layer, Presentation.Tests, and SubSonic data access layer. This sample is simplistic and should be used as a demo. I may be doing some things in code for the sake of brevity and to simplify the concepts. This is my disclaimer for not providing "production" code with all the frameworks, tools, and layers I typically create.
History
Initial Upload: November 6, 2007.