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.
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.
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.
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.
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
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.
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).
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:
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.
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
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.
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.