Click here to Skip to main content
Click here to Skip to main content

Advancing the Model-View-Presenter Pattern - Fixing the Common Problems

, 6 Sep 2007
Rate this:
Please Sign up or sign in to vote.
Addressing the common issues related to the MVP pattern using ASP.NET and WinForms clients.

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.

/// <summary>
/// Base functionality all Presenters should support
/// </summary>
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;
        }
    } 
    /// <summary>
    /// Converts an object from IView to the type of view the Presenter expects
    /// </summary>
    /// <typeparam name="T">Type of view to return (i.e. ILoginView)</typeparam>
    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;

    // Add the session as a parameter if it's not null

    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.

/// This is in the presentation layer.
public interface ISessionProvider
{
    object this[string name] { get;set;}
    object this[int index] { get;set;}
} 

/// This is in the asp.net project. It's a wrapper for the HttpSessionState object
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

TylerBrinks
Web Developer PageLabs
United States United States
I'm the founder of PageLabs, a web-based performance and SEO optimization site.

Give your site a boost in performance, even take a free speed test!
 
http://www.pagelabs.com
Follow on   Twitter

Comments and Discussions

 
GeneralRe: Use Events Instead of Execute PinmemberGlyn Darkin18-Jun-07 4:05 
GeneralWebsessionprovider Pinmemberkaspyanand18-Jul-06 22:46 
GeneralRe: Websessionprovider PinmemberAcoustic19-Jul-06 2:10 
GeneralRe: Websessionprovider PinmemberAcoustic19-Jul-06 8:12 
GeneralRe: Websessionprovider Pinmemberkaspyanand20-Jul-06 7:43 
GeneralRe: Websessionprovider PinmemberAcoustic20-Jul-06 9:10 
GeneralRe: Websessionprovider Pinmemberkaspyanand20-Jul-06 21:04 
GeneralRe: Websessionprovider PinmemberAcoustic21-Jul-06 2:41 
Yes, I meant to say that views should not use domain model objects. Presenters should translate objects into standard framework types to pass to the views.
 
Notion ONE - Excellence through Service.
GeneralRe: Websessionprovider Pinmembermrjmwcom16-Oct-06 5:31 
GeneralRe: Websessionprovider Pinmemberpliebscher2-Jan-07 7:15 
QuestionWhat about self documentation? PinmemberThomas Eyde17-Jul-06 12:14 
AnswerRe: What about self documentation? PinmemberAcoustic17-Jul-06 16:31 
GeneralRe: What about self documentation? PinmemberDylan Thomas17-Aug-07 5:56 
GeneralGood article... PinmemberMaruis Marais17-Jul-06 11:40 
GeneralRe: Good article... PinmemberAcoustic17-Jul-06 16:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 6 Sep 2007
Article Copyright 2006 by TylerBrinks
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid