Model View Presenter
As UI-creation technologies such as ASP.NET and Windows® Forms become more and more powerful, it’s common practice to let the UI layer do more than it should. Without a clear separation of responsibilities, the UI layer can often become a catch-all for logic that really belongs in other layers of the application. One design pattern, the Model View Presenter (MVP) pattern, is especially well suited to solving this problem. In order to illustrate my point, I will build a display screen that follows the MVP pattern for customers in the Northwind database.
Why is it bad to have lots of logic in the UI layer? The code in the UI layer of an application is very difficult to test without either running the application manually or maintaining ugly UI runner scripts that automate the execution of UI components. While this is a big problem in itself, an even bigger problem is the reams of code that are duplicated between common views in an application. It can often be hard to see good candidates for refactoring when the logic to perform a specific business function is copied among different pieces in the UI layer. The MVP design pattern makes it much easier to factor logic and code out of the UI layer for more streamlined, reusable code that’s easier to test.
Figure 1 shows the main layers that make up the sample application. Notice that there are separate packages for UI and presentation. You might have expected them to be the same, but actually, the UI layer of a project should consist only of the various UI elements—forms and controls. In a Web Forms project, this is typically a collection of ASP.NET Web Forms, user controls, and server controls. In Windows Forms, it is a collection of Windows Forms, user controls, and third-party libraries. This extra layer is what keeps the display and the logic separate. In the presentation layer, you have the objects that actually implement the behaviour for the UI—things like validation display, collection input from the UI, and so forth.
Figure 1: Application Architecture
Following the MVP
As you can see in Figure 2, the UI for this project is pretty standard. When the page loads, the screen will display a dropdown box filled with all of the customers in the Northwind database. If you select a customer from the dropdown list, the page will update to display the information for that customer. By following the MVP design pattern you can factor behaviours out of the UI and into their own classes. Figure 3 shows a class diagram that indicates the association between the different classes that are involved.
Figure 2: Customer Information
Figure 3: MVP Class Diagram
[Test] public void ShouldLoadListOfCustomersOnInitialize() { mockery = new Mockery(); ICustomerTask mockCustomerTask = mockery.NewMock<ICustomerTask>(); IViewCustomerView mockViewCustomerView = mockery.NewMock<IViewCustomerView>(); ILookupList mockCustomerLookupList = mockery.NewMock<ILookupList>(); ViewCustomerPresenter presenter = new ViewCustomerPresenter(mockViewCustomerView, mockCustomerTask); ILookupCollection mockLookupCollection = mockery.NewMock<ILookupCollection>(); Expect.Once.On(mockCustomerTask).Method( "GetCustomerList").Will(Return.Value(mockLookupCollection)); Expect.Once.On(mockViewCustomerView).GetProperty( "CustomerList").Will(Return.Value(mockCustomerLookupList)); Expect.Once.On(mockLookupCollection).Method( "BindTo").With(mockCustomerLookupList); presenter.Initialize(); }
Figure 4: The First Test
Making the First Test Pass
One of the advantages of writing the test first is that I now have a blueprint (the test) I can follow to get the test to compile and eventually pass. The first test has two interfaces that do not yet exist. These interfaces are the first prerequisites for getting the code to compile correctly. I’ll start with the code for IViewCustomerView:
public interface IViewCustomerView { ILookupList CustomerList { get; } }
This interface exposes one property that returns an ILookupList interface implementation. I don’t yet have an ILookupList interface or even an implementer, for that matter. For the purpose of getting this test to pass, I don’t need an explicit implementor, so I can proceed to create the ILookupList interface:
public interface ILookupList { }
public class ViewCustomerPresenter { private readonly IViewCustomerView view; private readonly ICustomerTask task; public ViewCustomerPresenter( IViewCustomerView view, ICustomerTask task) { this.view = view; this.task = task; } public void Initialize() { throw new NotImplementedException(); } }
Figure 5: Compiling the Test
As mentioned already, I am not coding the presenter blindly; I already know, from looking at the test, what behaviour the presenter should exhibit once the initialize method is called. The implementation of that behaviour is as follows:
public void Initialize() { task.GetCustomerList().BindTo(view.CustomerList); }
Filling the DropDownList
public class LookupCollection : ILookupCollection { private IList<ILookupDTO> items; public LookupCollection(IEnumerable<ILookupDTO> items) { this.items = new List<ILookupDTO>(items); } public int Count { get { return items.Count; } } public void BindTo(ILookupList list) { list.Clear(); foreach (ILookupDTO dto in items) list.Add(dto); } }
Figure 6: The LookupCollection Class
The implementation of the BindTo method is of particular interest. Notice that in this method the collection iterates through its own private list of ILookupDTO implementations. An ILookupDTO is an interface that caters well to binding to comboboxes in the UI layer:
public interface ILookupDTO { string Value { get; } string Text { get; } }
[Test] public void ShouldBeAbleToBindToLookupList() { IList<ILookupDTO> dtos = new IList; ILookupList mockLookupList = mockery.NewMock<ILookupList>(); Expect.Once.On(mockLookupList).Method("Clear"); for (int i = 0; i < 10; i++) { SimpleLookupDTO dto = new SimpleLookupDTO(i.ToString(),i.ToString()); dtos.Add(dto); Expect.Once.On(mockLookupList).Method("Add").With(dto); } new LookupCollection(dtos).BindTo(mockLookupList); }
Figure 7: A Test that Describes Behavior
[Test] public void ShouldAddItemToUnderlyingList() { ListControl webList = new DropDownList(); ILookupList list = new WebLookupList(webList); SimpleLookupDTO dto = new SimpleLookupDTO("1","1"); list.Add(dto); Assert.AreEqual(1, webList.Items.Count); Assert.AreEqual(dto.Value, webList.Items[0].Value); Assert.AreEqual(dto.Text, webList.Items[0].Text); }
Figure 8: First Test for WebLookupList Control
Figure 9: WebLookupList Class
public class WebLookupList : ILookupList { private ListControl underlyingList; public WebLookupList(ListControl underlyingList) { this.underlyingList = underlyingList; } public void Add(ILookupDTO dto) { underlyingList.Items.Add(new ListItem(dto.Text, dto.Value)); } }
Figure 10: WebLookupList Control
Remember, one of the keys to MVP is the separation of layers introduced by the creation of a view interface. The presenter doesn’t know what implementation of a view, and respectively an ILookupList, it will be talking to; it just knows that it will be able to call any of the methods defined by those interfaces. Ultimately, the WebLookupList class is a class that wraps and delegates to an underlying ListControl (base class for some of the ListControls defined in the System.Web.UI.WebControls project). With that code now in place, I can compile and run the WebLookupList control test which should pass. I can add one more test for the WebLookupList that tests the actual behaviour of the clear method:
[Test] public void ShouldClearUnderlyingList() { ListControl webList = new DropDownList(); ILookupList list = new WebLookupList(webList); webList.Items.Add(new ListItem("1", "1")); list.Clear(); Assert.AreEqual(0, webList.Items.Count); }
Again I am testing that the WebLookupList class will actually change the state of the underlying ListControl (DropDownList) when its own methods are invoked. The WebLookupList is now featured complete for the purposes of populating a DropDownList in a Web Form. It is now time for me to wire everything together and get the Web page’s dropdown filled with a list of customers.
Implementing the View Interface
Because I am building a Web Forms front end, it makes sense that the implementer for the IViewCustomerView interface would be a Web Form or user control. For the purpose of this column, I’ll make it a Web Form. The general appearance of the page has already been created, as you saw in Figure 2. Now I need only to implement the view interface. Switching to the code-behind for the ViewCustomers.aspx page, I can add the following code, indicating that the page is required to implement the IViewCustomersView interface:
public partial class ViewCustomers : Page,IViewCustomerView
By choosing to implement the IViewCustomerView interface, our Web page now has a responsibility to implement any methods or properties defined by that interface. Currently, there is only one property on the IViewCustomerView interface and that is a getter that returns any implementation of an ILookupList interface. I added a reference to the Web. Controls project so that I can instantiate a WebLookupListControl. I did this because the WebLookupListControl implements the ILookupList interface and it knows how to delegate to actual WebControls that live in ASP.NET. Taking a look at the ASPX for the ViewCustomer page, you will see that the list of customers is simply an asp: DropDownList control:
<td>Customers:</td> <td><asp:DropDownList id="customerDropDownList" AutoPostBack="true" runat="server" Width="308px"></asp:DropDownList></td> </tr>
With this already in place, I can quickly continue to implement the code required to satisfy the implementation of the IViewCustomerView interface:
public ILookupList CustomerList { get { return new WebLookupList(this.customerDropDownList);} }
The actual instantiation of the presenter is going to take place in the code-behind for the Web page. This is a problem because the UI project has no reference to the service layer project. The presentation project does, however, have a reference to the service layer project. This allows me to solve the problem by adding an overloaded constructor to the ViewCustomerPresenterClass:
public ViewCustomerPresenter(IViewCustomerView view) : this(view, new CustomerTask()) {}
This new constructor satisfies the presenter’s requirement for implementations of both the view and the service, while also maintaining the separation of the UI layer from the service layer. It is now fairly trivial to finish off the code for the code-behind:
protected override void OnInit(EventArgs e) { base.OnInit(e); presenter = new ViewCustomerPresenter(this); } protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) presenter.Initialize(); }
Notice the key to the instantiation of the presenter is the fact that I am making use of the newly created overload for the constructor, and the Web Form passes itself in as an object that implements the view interface!
[Test] public void ShouldDisplayCustomerDetails() { SimpleLookupDTO lookupDTO = new SimpleLookupDTO("1","JPBOO"); CustomerDTO dto = new CustomerDTO("BLAH", "BLAHCOMPNAME", "BLAHCONTACTNAME", "BLAHCONTACTTILE", "ADDRESS", "CITY", "REGION", "POSTALCODE", Country.CANADA, "4444444", "4444444"); Expect.Once.On(mockViewCustomerView).GetProperty( "CustomerList").Will(Return.Value(mockCustomerLookupList)); Expect.Once.On(mockCustomerLookupList).GetProperty( "SelectedItem").Will(Return.Value(lookupDTO)); Expect.Once.On(mockCustomerTask).Method( "GetDetailsForCustomer").With(1).Will(Return.Value(dto)); Expect.Once.On(mockViewCustomerView).SetProperty( "CompanyName").To(dto.CompanyName); Expect.Once.On(mockViewCustomerView).SetProperty( "ContactName").To(dto.ContactName); Expect.Once.On(mockViewCustomerView).SetProperty( "ContactTitle").To(dto.ContactTitle); Expect.Once.On(mockViewCustomerView).SetProperty( "Address").To(dto.Address); Expect.Once.On(mockViewCustomerView).SetProperty( "City").To(dto.City); Expect.Once.On(mockViewCustomerView).SetProperty( "Region").To(dto.Region); Expect.Once.On(mockViewCustomerView).SetProperty( "PostalCode").To(dto.PostalCode); Expect.Once.On(mockViewCustomerView).SetProperty( "Country").To(dto.CountryOfResidence.Name); Expect.Once.On(mockViewCustomerView).SetProperty( "Phone").To(dto.Phone); Expect.Once.On(mockViewCustomerView).SetProperty("Fax").To(dto.Fax); presenter.DisplayCustomerDetails(); }
Figure 11: One Last Test
As before, I am taking advantage of the NMock library to create mocks of the task and view interfaces. This particular test verifies the behaviour of the presenter by asking the service layer for a DTO representing a particular customer. Once the presenter retrieves the DTO from the service layer, it will update properties on the view directly, thus eliminating the need for the view to have any knowledge of how to correctly display the information from the object. For brevity I am not going to discuss the implementation of the SelectedItem property on the WebLookupList control; instead, I will leave it to you to examine the source code to see the implementation details. What this test really demonstrates is the interaction that occurs between the presenter and the view once the presenter retrieves a CustomerDTO from the service layer. If I attempt to run the test now, I will be in a big failure state as a lot of the properties don’t yet exist on the view interface. So I’ll go ahead and add the necessary members to the IViewCustomerView interface, as you see in Figure 12.
public interface IViewCustomerView { ILookupList CustomerList{get;} string CompanyName{set;} string ContactName{set;} string ContactTitle{set;} string Address{set;} string City{set;} string Region{set;} string PostalCode{set;} string Country{set;} string Phone{set;} string Fax{set;} }
Figure 12: Completing the IVewCustomerView Interface
Immediately after adding these interface members, my Web Form complains because it is no longer fulfilling the contract of the interface, so I have to go back to the code-behind for my Web Form and implement the remaining members. As stated before, the entire markup for the Web page has already been created, as have the table cells which have been marked with the “runat=server” attribute and are named according to the information that should be displayed in them. This makes the resulting code to implement the interface members very trivial:
public string CompanyName { set { this.companyNameLabel.InnerText = value; } } public string ContactName { set { this.contactNameLabel.InnerText = value; } } ...
With the setter properties implemented, there is just one thing left to do. I need a way to tell the presenter to display the information for the selected customer. Looking back at the test, you can see that the implementation of this behaviour will live in the DisplayCustomerDetails method on the presenter. This method will not, however, take any arguments. When invoked, the presenter will turn back around to the view, pull from it any information it needs (which it will retrieve by using the ILookupList), and then use that information to retrieve the details about the customer in question. All that I need to do from a UI perspective is set the AutoPostBack property of the DropDownList to true, and I also need to add the following event handler hookup code to the OnInit method of the page:
protected override void OnInit(EventArgs e) { base.OnInit(e); presenter = new ViewCustomerPresenter(this); this.customerDropDownList.SelectedIndexChanged += delegate { presenter.DisplayCustomerDetails(); }; }
This event handler ensures that whenever a new customer is selected in the dropdown, the view will ask the presenter to display the details for that customer.
public void DisplayCustomerDetails() { int? customerId = SelectedCustomerId; if (customerId.HasValue) { CustomerDTO customer = task.GetDetailsForCustomer(customerId.Value); UpdateViewFrom(customer); } } private int? SelectedCustomerId { get { string selectedId = view.CustomerList.SelectedItem.Value; if (String.IsNullOrEmpty(selectedId)) return null; int? id = null; try { id = int.Parse(selectedId.Trim()); } catch (FormatException) {} return id; } } private void UpdateViewFrom(CustomerDTO customer) { view.CompanyName = customer.CompanyName; view.ContactName = customer.ContactName; view.ContactTitle = customer.ContactTitle; view.Address = customer.Address; view.City = customer.City; view.Region = customer.Region; view.Country = customer.CountryOfResidence.Name; view.Phone = customer.Phone; view.Fax = customer.Fax; view.PostalCode = customer.PostalCode; }
Figure 13: Completing the Presenter
What’s Next?
Send your questions and comments to mmpatt@microsoft.com.