Introduction
Before you read this article, make sure you're caught up on Billy McCafferty's article on the MVP pattern. Not only does this article take a large inspiration from Bill's work, but he put a lot of effort into one of the better articles recently posted on The Code Project. Thanks, Billy!
On to the task at hand.
The Problem
Like most emerging patterns, spending even a little time with the MVP pattern reveals both strengths and weaknesses. Billy does well to explain the benefits of keeping the ASP.NET pipeline in tact and the obvious ease of unit testing. One huge benefit not yet addressed is presentation layer reusability (see the sample WinForms app). But for all its benefits, the MVP pattern, as people understand it in its early stages, has its share of problems.
Based on what I see as the public's current understanding of the MVP pattern, let's list a few of the major weaknesses:
- No Code Reuse - Each view (page/control) must create an instance of a specific presenter in order to invoke the presenter's methods. That's four or five lines of code per page/control; quite a bit of work if you have hundreds of pages and controls in your site. Code can and should be centralized.
- Presenter Creation - Presenters operate on a specific type of interface. Generally speaking, there's a one-to-one relationship between presenters and interfaces. This problem relates to the "no code reuse" point, and leads to inconsistent public-facing functionality exposed by various presenters. Object creation should be standardized.
- View Intelligence – Using the MVP pattern forces a view to know as much about its presenter (methods, properties etc…) as the presenter knows about its view. The use of interfaces prevents a circular reference, but there should still be further decoupling of the view. One area I may disagree with Bill is that views should also not know what data layer type (or DAO interface type) to pass to a presenter. I'm of the opinion that the views shouldn't have a reference to any data layer (i.e., anything upstream of the presenters).
- State Management – This one is a biggie. Many of the people posting on MVP are quick to point out that (very simple) MVP examples remove the ability for ASP.NET to use session and caching. How do you access context-specific information if presenters can't have a reference to anything downstream (i.e.,
System.Web
or System.Windows.Forms
)? The presentation layer should provide a way to maintain application state.
The Challenge
Let's tackle each of these problems head on, one-by-one. By the end of this example, we should have the beginnings of a more matured approach to MVP as it applies to .NET.
No Code Reuse: Every single page contains an instance of a presenter. That's quite a bit of copy-paste if you want to convert a large website to use MVP. The obvious place to start is by creating a base class for all presenters that all the views can share. We'll add an abstract base presenter to the presentation project. The constructor takes a generic IView
interface and provides a method (via Generics) to cast the view to a more specific interface type. To keep the view's logic as simple as possible, the base presenter will expose a single abstract method, "Execute
". Each concrete presenter will be responsible for the implementation details for Execute
, but any view can now call Execute
without having to know anything about the type of presenter it's calling.
public abstract class Presenter
{
protected readonly IView _view;
public Presenter( IView view ) : this(view, null)
{ }
public Presenter( IView view, ISessionProvider session )
{
_view = view;
if(session != null)
{
SessionManager.Current = session;
}
}
protected T GetView<T>() where T : class, IView
{
return _view as T;
}
protected ISessionProvider Session
{
get { return SessionManager.Current; }
}
}
Now that there's a base implementation for the presentation layer, a base web page is in order. We should be able to get rid of those four or five lines of code required by each page and move it to a few lines in the base page. On a site with 300 pages and controls, we just saved 1200 lines of code! In short, the base page provides two methods to facilitate registering a view with the associated presenter.
public class BasePage : System.Web.UI.Page, IView
{
protected T RegisterView<T>() where T : Presenter
{
return PresentationManager.RegisterView<T>(typeof(T),
this, new WebSessionProvider());
}
protected void SelfRegister(System.Web.UI.Page page)
{
if (page != null && page is IView)
{
object[] attributes =
page.GetType().GetCustomAttributes(typeof(PresenterTypeAttribute), true);
if (attributes != null && attributes.Length > 0)
{
foreach(Attribute viewAttribute in attributes)
{
if (viewAttribute is PresenterTypeAttribute)
{
PresentationManager.RegisterView((viewAttribute
as PresenterTypeAttribute).PresenterType,
page as IView, new WebSessionProvider());
break;
}
}
}
}
}
}
The base page provides two differing ways of view registration. A view can pass in the type of presenter to load, along with an instance of itself, to the RegisterView<T>
method. This is really more of a left over convention from the original code this article was based on. The more friendly method of registration is done by a page calling the SelfRegister
method and passing itself, as an instance, as the solitary argument. The SelfRegister
method then examines the page's attributes to find the correct presenter type to load.
Presenter Creation: Now that the base web page handles calls to the Presentation Manager, there's no reason why presenter creation should be complex. A simple presenter factory would be a clean and easy way to standardize how presenters are created. All we need to know is the type of interface the view implements. That's enough information to create the appropriate presenter.
public static class PresentationManager
{
public static T RegisterView<T>(Type presenterType,
IView view) where T : Presenter
{
return RegisterView<T>(presenterType, view, null);
}
public static T RegisterView<T>(Type presenterType, IView view,
ISessionProvider session) where T : Presenter
{
return LoadPresenter(presenterType, view, session) as T;
}
public static void RegisterView(Type presenterType, IView view)
{
RegisterView(presenterType, view, null);
}
public static void RegisterView(Type presenterType,
IView view, ISessionProvider session)
{
LoadPresenter(presenterType, view, session);
}
private static object LoadPresenter(Type presenterType,
IView view, ISessionProvider session)
{
int arraySize = session == null ? 1 : 2;
object[] constructerParams = new object[arraySize];
constructerParams[0] = view;
if (arraySize.Equals(2))
{
constructerParams[1] = session;
}
return Activator.CreateInstance(presenterType, constructerParams);
}
}
For this example, and for simplicity, I put a custom attribute on each view that defines what view interface type it operates on. This makes object creation easy, fast, and a great candidate for caching after the first call. A more complex example or framework may use a custom configuration section for more flexible mapping. This example also doesn't account for mapping multiple presenters to a single view; something that may be necessary for a real MVP framework.
View Intelligence: Our base page class helps to reduce the view's registration logic, but there's more to tidy up to be done. The view doesn't need to know any details of the presenter's methods and properties (remember, no up-stream references). The view only needs to know the operations it wants to perform, and those are defined in the respective interfaces. Let's take the common task of logging in to a system. Before our modifications, the code would look like this:
protected void loginButton_Click( object sender, EventArgs e)
{
_loginPresenter.LoginUser(this.userNameTextBox.Text,
this.passwordTextBox.Text);
}
This isn't bad, but the view has to explicitly give the presenter the requited data. A cleaner approach would be for the view to request a type of execution and let the presenter get the data it needs.
public event EventHandler OnLogin;
protected void Page_Load(object sender, EventArgs e)
{
base.SelfRegister(this);
}
protected void loginButton_Click( object sender, EventArgs e)
{
if(this.OnLogin != null)
{
OnLogin(this, EventArgs.Empty);
}
}
Remember that one of the goals of MVP is to truly separate responsibilities. Letting the presenter respond to events fired by the view for data truly puts the responsibility on the presenter to take action. This design also works better for unit testing, because your mock views in your unit test will more closely match your real-world views. Your unit tests can raise the same events, thereby simulating your UI very closely.
State Management: This is the topic that seems to pop up each time I read about someone's problems with the MVP pattern. The misconception is that there's no way to use Session, Cache, etc… without forcing the presentation project to have a reference to System.Web
. I think that's a bit shortsighted.
Actually, the answer is rather simple. Views interact with the presentation layer via the interfaces they implement. Application state (Session) shouldn't be any different. Simply put, all we need is a state management interface that defines how to interact with any state object. Then, just like a view implements an interface, ASP.NET's Session
object can be wrapped in a class that implements the state management interface.
public interface ISessionProvider
{
object this[string name] { get;set;}
object this[int index] { get;set;}
}
public class WebSessionProvider : ISessionProvider
{
private HttpSessionState Session
{
get { return HttpContext.Current.Session; }
}
public void Add( string name, object value )
{
Session.Add(name, value);
}
public void Clear()
{
Session.Clear();
}
public bool Contains( string name )
{
return Session[name] != null;
}
object ISessionProvider.this[string name]
{
get{ return Session[name];
}
{
set { Session[name] = value; }
}
object ISessionProvider.this[int index]
{
get{return Session[index]; }
set{ Session[index] = value; }
}
}
Since presenters interact with interfaces, and state has been abstracted to adhere to an interface, our presentation layer now has access to any type of state management without ever having to know the details of the state's internals… including the ASP.NET Session
object (see the WinForms sample for a custom state example).
Where are We?
I said that by the end of the article we'd have the beginnings of maturing how MVP applies to .NET. Are we there yet... I don't know. I think there's still plenty of work left to be done. MVP is a relatively new approach (though MVC has been around the block many times), and the best ideas haven't been thought of yet. My hope is these ideas give you a leg up if you're looking to use the pattern and want to make it clean and usable. Better yet, what ideas do you have on how MVP can be improved upon? Take a look at the sample projects to see how the layers are truly interacting with each other. Then, more importantly, share your thoughts with your own super cool article.