Wednesday, October 29, 2008

Model View Presenter (MVP) Design Pattern with .NET - Winforms vs. ASP.NET Webforms

MVP implementation for .NET desktop/smart-client (winforms) applications can take the form of Supervising Controller or Passive View. The main difference between the patterns is that 'Supervising Controller' encourages coupling between the View and the Model (via Observer-Synchronization) while 'Passive View' forbids it.

Typically, we'll chose 'Supervising Controller' if we have state full Model and we need the Views to immediately synchronize with it through the observer/observable mechanism, in this case we might be wiling to cut down on testability and allow direct link between the View and the Model.

image

Notice that the View is prompted to update its display by both Presenter (directly) and Model (via event). In most cases the view responds to simple changes in Model state and update itself, when more complex logic is involved -the Presenter takes the liberty to change the View according to application rules.    

'Passive View' will be preferred if we want to test all presentation behavior; this pattern encourages thin Views that contain no logic thus can be easily mocked and dominant Presenter that is in charge of the entire workflow (and all updates on the View) thus can be effectively tested.

image 

Notice that in case the Model is stateless (usually in web style application) it doesn't raise 'State Changed' event (as there is no state).

In ASP.NET (webforms), due to the stateless nature of the web and the unique working method of ASP.NET - it rarely makes sense to use 'Supervising Controller' as the Model is often stateless and never used as an observer, thus the only dissent alternative for webforms applications is to use 'Passive View'. From this we can conclude that if winforms Presenters/Models are to be reused in webforms application - its probably best to implement the winforms application as 'Passive View' that is applicable for both.

This article reviews the 'Supervising Controller' implementation of MVP in winforms applications and the 'Passive View' implementation in ASP.NET webforms applications. All the code in the scope of this article is available for download.

To get more background, you can read about MVP implementation in desktop applications in 'Twisting the MVC Triad', and about MVP in web applications in 'MVP for Web Applications' (for more links- see above).

MVP in .NET Winforms

Since the 'Supervising Controller' approach encourages communication between the View and the Model its best use is where the Views need to immediately synchronize with 'sudden' changes in Model data (changes which haven't been triggered by the Presenter). In such case we'll rather have the Views registered directly to Model events rather than though the Presenter, this way we won't have to make cascading changes when ever model data structure changes.

In most cases the workflow starts from the View, through the Presenter to the Model, and back to the view via the Observer/Observable mechanism. In others, the workflow starts already from the Model and to the view via the Observer/Observable mechanism.

image_thumb1 

How does it work?

1) View delegates user inputs to the Presenter -> Presenter performs some UI related calculation, changes internal UI related state, changes the View directly, and command the Model as appropriate

"in smart client applications the Model is merely a proxy for the real Model that is placed in the server"

2) Model updates its state, performs some business operation and raises the proper event -> View handles the event - it asks relevant data from the Model and changes its display.

Normally, the view creates the Presenter and they both observe and interact with some singleton Model, the latter is also being observed by other Views/Presenters of the application.

Let's examine winforms implementation for 'Add Customer' view.

image

The Model allows getting/saving data from/to the data source, it stores cached data and raises events to notify its clients about changes in its data.

// Long lasting observable with caching.
public class CustomerDao: ICustomerDao
{
    private CustomerDataMapper m_dataMapper;
    private List<Customer> m_allCustomers;
    private const string FILE_NAME = "Customers.xml";

    public event EventHandler<CustomerSavedEventArgs> CustomerSaved;

    public CustomerDao()
    {
        m_dataMapper = new CustomerDataMapper(FILE_NAME);
        m_allCustomers = m_dataMapper.GetAllCustomers();
    }

    public IEnumerable<Customer> GetAllCustomers()
    {
        return m_allCustomers;
    }

    public void Save(Customer p_customer)
    {
        m_dataMapper.Save(p_customer);
        m_allCustomers.Add(p_customer);
        raiseCustomerSaved(p_customer);
    }

    public Customer GetByName(string p_name)
    {
        foreach (Customer customer in m_allCustomers)
        {
            if (customer.ContactName == p_name)
            {
                return customer;
            }

        }

        return null;
    }

    private void raiseCustomerSaved(Customer p_customer)
    {
        if (CustomerSaved != null)
        {
            CustomerSaved(this, new CustomerSavedEventArgs(p_customer));
        }
    }
}

public class CustomerSavedEventArgs : EventArgs
{
    private readonly Customer m_customer;

    public CustomerSavedEventArgs(Customer p_customer)
    {
        m_customer = p_customer;
    }

    public Customer Customer
    {
        get { return m_customer; }
    }
}

The View creates the Presenter and injected with the Model, it delegates user inputs to the Presenter and observe the Model for changes in its data.

public partial class AddCustomerWinformsView : Form, IAddCustomerView
{
    private readonly ICustomerDao m_customerDao;
    private AddCustomerPresenter m_presenter;

    public AddCustomerWinformsView(ICustomerDao p_CustomerDao)
    {
        InitializeComponent();

        AddCustomerPresenter presenter = new AddCustomerPresenter(this, p_CustomerDao);
        m_presenter = presenter;

        m_customerDao = p_CustomerDao;
        registerToModelEvents();
    }


    public void AddCustomerToList(Customer p_customer)
    {
        ListViewItem item = new ListViewItem(p_customer.ContactName);
        item.SubItems.Add(p_customer.CompanyName);
        listView1.Items.Add(item);
    }

    public Customer CustomerToAdd
    {
        get
        {
            return new Customer(textBoxName.Text, textBoxCompany.Text);
        }
    }

    public string Message
    {
        get
        {
            return lblMessage.Text;
        }
        set
        {
            lblMessage.Text = value;
        }
    }

    private void btnAdd_Click(object sender, EventArgs e)
    {
        m_presenter.AddCustomer();
    }

    void m_customerDao_CustomerSaved(object sender, CustomerSavedEventArgs e)
    {
        AddCustomerToList(e.Customer);
    }


    private void registerToModelEvents()
    {
        m_customerDao.CustomerSaved += m_customerDao_CustomerSaved;
    }
}

The Presenter mediate between the View and the Model, it accepts gestures from the View and command the Model as appropriate.

public class AddCustomerPresenter
{
    private IAddCustomerView m_view;
    private ICustomerDao m_customerDao;

    public AddCustomerPresenter(IAddCustomerView p_view,
        ICustomerDao p_customerDao)
    {
        m_view = p_view;
        m_customerDao = p_customerDao;
    }

    public void InitView()
    {
        m_view.Message = "Use this form to add a new customer.";

        IEnumerable<Customer> customers = m_customerDao.GetAllCustomers();
        foreach (Customer customer in customers)
        {
            m_view.AddCustomerToList(customer);
        }
    }

    /// <summary>
    /// Called by the view; this grabs the updated customer from the view and commits it to the DB.
    /// </summary>
    public void AddCustomer()
    {
        if (!IsDuplicateOfExisting(m_view.CustomerToAdd))
        {
            m_customerDao.Save(m_view.CustomerToAdd);
        }
        else
        {
            m_view.Message = "The ID you provided is already in use." +
                "Please change the ID and try again.";
        }
    }

    /// <summary>
    /// Checks if a customer already exists with the same customer ID.
    /// </summary>
    private bool IsDuplicateOfExisting(Customer newCustomer)
    {
        Customer duplicateCustomer = m_customerDao.GetByName(newCustomer.ContactName);
        return duplicateCustomer != null;
    }
}

Sequence

image

Pay attention to the following:

1) The View creates the Presenter, it lives as long as it's visible to the front end user.

2) Both View and Presenter reference the Model which act as an observable. Indeed, one of the most  important principles of MVC/P is that Model should supply mechanism to allow multiple Views to observe its data.

4) The Model is unaware of nether the View nor the Presenter.

3) The presenter mediate between the View and the Model.

View and Presenter coupling

In this example the View and the Presenter are tightly coupled, the View holds reference the Presenter and access it directly and and the Presenter holds reference the View abstraction (for testing sake).  However,  there is an alternative in which only the Presenter is tightly coupled to the View while the View is not aware of the Presenter. 

You can read more about the coupling between the View and the Presenter in Model View Presenter Styles.

Smart Client Applications

MVP was designed to fit both desktop and smart-client applications (read more). In desktop applications its abstractions reside in one node (such as demonstrated above) while in smart-client web applications they scattered around two or more nodes. In most cases the View and the Presenter reside in the client and the Model is being split between the client and server [1]

MVP in ASP.NET (Browser based) Applications

In scalable browser-based web-applications, a client interact with 1..n servers where each requests coming from the client can be satisfied by a different server. 

image

ASP.NET supplies advanced framework that shields developers from dealing with client-server communication and client rendering by making the development style of web applications (webforms) look very much like the convenient style of desktop applications (winforms).

image

When developing winforms application the stating point is the Form, we start by dropping controls (widgets) on the form (view) surface, registering to the proper events and implementing the handler on the form code-behind; with ASP.NET it's pretty much the same deal, drop web control on aspx Page, register to the proper event and implement the handler on the page (view) code-behind. The big difference is that with web-forms we have to consider the stateless nature of the web, thus deal with the fact that the page is re-created on each post-back; but still, in both webforms and winforms the starting point is the view (page/form) and in both we really don't want the view to handle any kind of application/business logic.

In webforms applications - the View, the Presenter and the Model are recreated on each postback, thus MVP cannot be implemented as Supervising Controller. Instead, it's usually implemented as Passive View:

image

It goes like this: the client submits request (user press on control (e.g. button) that run-at-server), the View accept the request, it creates the Presenter and the Model and delegate the request to the Presenter, the Presenter gets current state from the View (textbox text, listbox selection etc), performs some UI related calculation, changes the View directly, and command the Model as appropriate, it than prompt the View to update its display. 

Since the Model is re-created on each postback - the Model is not an Observable by any mean; I guess that this is the biggest different between the typical winforms Model (that is state full thus can act as an observer) and the typical webforms Model.

Let's examine the webforms implementation for 'Add Customer' view.

image

The Model is passive, it allows saving customer data to DB and getting customer from DB (by ID).

// Static Dao. Recreated on each user session
public class CustomerDao: ICustomerDao
{
    private const string FILE_NAME = "Customers.xml";

    public IEnumerable<Customer> GetAllCustomers()
    {
        CustomerDataMapper dataMapper = new CustomerDataMapper(FILE_NAME);

        return dataMapper.GetAllCustomers();
    }


    public void Save(Customer p_customer)
    {
        CustomerDataMapper dataMapper = new CustomerDataMapper(FILE_NAME);
        dataMapper.Save(p_customer);
    }

    public Customer GetByName(string p_name)
    {
        CustomerDataMapper dataMapper = new CustomerDataMapper(FILE_NAME);
        IEnumerable<Customer> customers = dataMapper.GetAllCustomers();
        foreach (Customer customer in customers)
        {
            if (customer.ContactName == p_name)
            {
                return customer;
            }

        }

        return null;

    }
}

The Presenter mediate between the View and the Model, it accepts gestures from the View and command the Model as appropriate, it then prompt the View to change accordingly.

public class AddCustomerPresenter
{
    private IAddCustomerView m_view;
    private ICustomerDao m_customerDao;

    public AddCustomerPresenter(IAddCustomerView p_view,
        ICustomerDao p_customerDao)
    {
        m_view = p_view;
        m_customerDao = p_customerDao;
    }

    public void InitView()
    {
        m_view.Message = "Use this form to add a new customer.";

        IEnumerable<Customer> customers = m_customerDao.GetAllCustomers();

        foreach (Customer customer in customers)
        {
            m_view.AddCustomerToList(customer);
        }
    }

    /// <summary>
    /// Called by the view; this grabs the updated customer from the view 
    /// and commits it to the DB.
    /// </summary>
    public void AddCustomer()
    {
        Customer customerToAdd = m_view.CustomerToAdd;
        
        if (IsDuplicateOfExisting(customerToAdd))
        {
            // By passing HTML tags from the presenter to the view, 
            // we've essentially bound the presenter to an HTML context.  
            // You may want to consider alternatives to keep the 
            // presentation layer web/windows agnostic.
            m_view.Message = 
                "<span style=\"color:red\">The ID you provided is " +
                "already in use.</span> Please change the ID and try again.";

            return;
        }

        m_customerDao.Save(customerToAdd);

        m_view.AddCustomerToList(customerToAdd);
    }

    /// <summary>
    /// Checks if a customer already exists with the same customer ID.
    /// </summary>
    private bool IsDuplicateOfExisting(Customer newCustomer)
    {
        Customer duplicateCustomer = 
            m_customerDao.GetByName(newCustomer.ContactName);
        
        return duplicateCustomer != null;
    }
}

The View is attached with Presenter and Model, it respond to user events (button click) and delegate the event to the Presenter.

public partial class AddCustomerWebformsView : UserControl, IAddCustomerView
{
    private AddCustomerPresenter m_presenter;

    public void AttachPresenter(AddCustomerPresenter presenter)
    {
        m_presenter = presenter;
    }

    public void AddCustomerToList(Customer p_customer)
    {
        ListBox1.Items.Add(p_customer.ContactName);
    }

    protected void btnAdd_OnClick(object sender, EventArgs e)
    {
        // Be sure to check isPageValid before anything else
        if (!Page.IsValid)
        {
            Message = "There was a problem with your inputs." +
                " Make sure you supplied everything and try again";
            return;
        }


        m_presenter.AddCustomer();
    }

    public Customer CustomerToAdd
    {
        get
        {
            Customer customer = 
                new Customer(txtContactName.Text, txtCompanyName.Text);
            
            return customer;
        }
    }

    public string Message
    {
        set
        {
            lblMessage.Text = value;
        }
    }
}

The page is the starting point of the application. On each postback (e.g. after user click on button that is configured to run-at-browser) ASP.NET creates new instance of the page, calls Page_Load, and calls the event handler of the control that initiate the postback (e.g. btnAdd_OnClick).

public partial class AddCustomerPage : Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        CustomerDao customerDao = new CustomerDao();

        AddCustomerPresenter presenter = 
            new AddCustomerPresenter(addCustomerWebformsView, customerDao);
        
        addCustomerWebformsView.AttachModel(customerDao);
        
        if (!IsPostBack)
        {
            // No need to update the display on each post back, 
            // as ViewState travel from client to server and back 
            // from server to client.
            presenter.InitView();
        }
    }
}

Sequence

image

Comparing the Presenters (Supervising Controller vs. Passive View)

As you can see - the only difference between the Presenters is that Passive View Presenter update the view after saving the data while Supervising Controller Presenter doesn’t, the View register to the appropriate model event and update itself.

image

Source code can be downloaded from here.


[1] Even though the paper describing MVP (written by Mike Potel in 1996) mentioned that MVP model can be used for browser-based applications (where some code is downloaded for execution on the client), it doesn't really explain how and it seems like a little thought was given to the subject. 

11 comments:

  1. Interesting article. The topic I have found to be missing these days on MVP for WinForms is how all the MVP parts work together. I mean you aren't going to implement one massive Application Presenter for an application comprised of sever components. Imagine how Word would be implemented as MVP.

    ReplyDelete
  2. There are some rules of thumb that I can point out. Most of them are not written in stone but from my experience can lead to more readable and usable code.

    UserControl is a View that is attached to one unique Presenter, there is no rule that says that each UserControl in the application must have its own Presenter (UserControl that acts as a View can be composition of couple of simple UserConrtols),on the contrary, be careful with creating too many Presenters or your code will turn into a nightmare.

    There maybe couple of Presenters for one view (to switch View functionality) - but only one should be active at a time.

    In some cases the same Presenter is used by more than one View (like when there is View for Right-To-Left language and the exact same View for Letf-To-Right) but in most cases one Presenter doesn’t suite two different Views of the same application (unless it is twisted which is not recommended). To avoid code duplication – it is often better to use some common helper class than to use one Presenter for two Views that are not exactly the same.

    Two Presenters can inherit from one abstract base class (but be careful with that); it is rarely a good idea to inherit from non abstract Presenter (that is attached to specific View).

    Presenter is usually attached with one singleton Model, the latter is shared among other Presenters.

    ReplyDelete
  3. Hi Aviad

    Thank you so much for putting together such a complete and coherent series of posts on MVC/MVP. Reading them has helped me finally understand these patterns and also to solve a problem (of my own making) with my work.

    Michael

    ReplyDelete
  4. Thanks This is what I am looking for.

    ReplyDelete
  5. Hi,

    Great article however the first link which contains the sample code no longer works.

    Is there a chance you can update it ?

    ReplyDelete
  6. I checked it and the link still works, the ADrive storage must have been down, since this is not the 1st time that happened I changed the storage provider to Google docs.

    Thanks for the notice,

    Aviad

    ReplyDelete
  7. Hi Michael,

    Thanks for the quick response. The link I was talking about was this paragraph

    "This article reviews the 'Supervising Controller' implementation of MVP in winforms applications and the 'Passive View' implementation in ASP.NET webforms applications. All the code in the scope of this article is available for download."

    I didn't notice the last download link.

    Thanks a lot!

    ReplyDelete
  8. Terribley sorry for getting your name incorrect Aviade !

    ReplyDelete
  9. Following with your example, could you explain how do you implement that if the list is in another form. For example, a form with a list of customer and the new, edit, delete buttons.
    When the user clik on the new or edit buttons, a new form is showed and the list is updated after the confirmation of the action, showing the modification or adding the new customer.

    ReplyDelete
  10. In MVP for winforms, where to keep such information like "Selected Customer" ?
    If several views need to display some information on the customer, should we duplicate the information in the presenters ( it means a copy per presenter)?
    I am reluctant to put this information in the model, which is shared by all presenters, so how to share without using the model ?
    thanks

    ReplyDelete
  11. Great article, it is nice to compare the two patterns through the real code. One thing that confused me is that instantiate the CustomerDao inside the View. Based on the pattern, should the View not aware of the Model? I think it is unnecessary to have the CustomerDao object inside the View, instead, it should be instantiated inside the Presenter.
    -Lee

    ReplyDelete