Design Mode: Model View Presenter Release Date: 2006-08-07 | updated on: 2006-08-07
Jean-Paul Boodhoo
Download the code in this article: DesignPatterns2006_08.exe (4423KB)
Content on this page
|
Follow MVP |
|
Make the first test pass |
|
Fill in DropDownList |
|
View Interface implementation |
|
Future Plan |
As the UI creation technology (such as ASP. NET and Windows Form) becomes more and more powerful, it has become a common practice to allow the UI Layer to execute more functions. Without clear division of duties, the UI Layer is often the all-around proxy of the logic layer, which actually belongs to other layers of the application. The Model View Presenter (MVP) mode is a design pattern specifically designed to solve this problem. To prove my point of view, I will follow the MVP mode to create a display screen for customers in the Northwind database.
Why shouldn't there be too many logic in the UI Layer? It is difficult to test the code in the application UI Layer if the application is not run manually or if the advanced UI running script of the UI component is not maintained automatically. This is an annoyance in itself, and a bigger headache is the large amount of repetitive code between common views in applications. When the logic for executing specific business functions is replicated between different parts of the UI Layer, it is usually difficult to find a good candidate for refactoring. The MVP design pattern makes separation of logic and code from the UI Layer easier, making testing reusable code easier.
Figure 1 shows the main layer of the sample application. Note that the UI Layer and presentation layer use different software packages. You may expect them to use the same software package, but in fact the UI Layer of a project should only consist of two types of UI elements-forms and controls. In Web Forms projects, it is usually a collection of ASP. NET Web Forms, user controls, and server controls. Windows Forms is a collection of Windows Forms, user controls, and third-party libraries. This additional layer is used to separate the display and logic. In the presentation layer, there can be objects that actually implement UI behavior, such as verification display and UI set input.
Figure 1 application architecture
Follow MVP
2. the UI of this project is very standard. When the page is loaded, a drop-down box filled with all customers in the Northwind database is displayed. If you select a customer from the drop-down list, the page is updated to display the customer information. By following the MVP design pattern, you can separate various behaviors from the UI Layer and place them into your own classes. Figure 3 shows a class chart, indicating the associations between different classes involved.
Figure 2 customer information
It is important to note that the provisioner does not know any knowledge about the actual UI Layer of the application. It knows that it can talk to interfaces, but it does not know or care about the specific implementation of interfaces. This promotes the reuse of the provisioner between different UI technologies.
I will use test-driven development (TDD) to create the client screen function. Figure 4 shows the details of the first test I will use to describe what I expect to observe on page loading. TDD allows me to focus on one problem at a time, write only enough code to pass the test, and then proceed. In this test, I will use a simulated object framework named NMock2 to build an interface simulation implementation.
Figure 3 MVP charts
In my MVP implementation, I decided to attach the dashboard to the view that will work with it. It is always good to create an object when the object can work immediately. In this application, the presentation layer actually relies on the service layer to call the domain function. Because of this requirement, it is also necessary to create an interface-based interpreter that can be used to communicate with the service class. This will ensure that once the provisioner is created, it can do all the work that needs it. I will start by creating two specific simulations: one for the service layer and the other for the view to be used by the provisioner.
Why do we need to create a simulation? Unit testing rules are to isolate tests as much as possible to concentrate on a specific object. In this test, I only focus on the expected behavior of the provisioner. At this time, I don't care about the actual implementation of the View Interface or service interface. I believe that the protocols defined by those interfaces will be simulated accordingly. This ensures that I focus my tests on the behavior of the provisioner that I expect without considering the objects on which it depends. After the initialization method is called, the behavior of the provisioner that I expect is as follows.
First, it indicates that the getmermerlist method on the ICustomerTask service layer object should be called (simulated in testing ). Note that you can use NMock to simulate simulated behavior. For the service layer, I want it to return the simulated ILookupCollection to the dashboard. After the provisioner retrieves ILookupCollection from the service layer, it should call the BindTo method of the collection and pass the method to the implementation of ILookupList. By using the nmocksponct. Once method, I can determine that the test will fail if the provisioner does not call this method Once (and only Once.
After writing this test, I will be completely unedited. I will try my best to do the simplest work to make the test pass.
Back to Top
Make the first test pass
One of the first advantages of writing a test is that I now have a vision blueprint that can be followed to compile and finally pass the test. The first test includes two interfaces that do not exist. These interfaces are a prerequisite for correct code compilation. I will start with the IViewCustomerView code:
public interface IViewCustomerView{ILookupList CustomerList { get; }}
This interface provides an attribute that can be returned by an ILookupList interface. I do not have an ILookupList interface or even an implementation tool. To pass this test, I do not need explicit implementation tools, so that I can continue to create the ILookupList interface: commit:
public interface ILookupList { }
At this time, the ILookupList interface seems useless. My goal is to compile and pass the test, and these interfaces can meet the test requirements. Now we should focus on ViewCustomerPresenter, the object to be tested. The struct class does not exist yet, but looking back at this test, you can come up with two important facts: it has a constructor, which requires the view and service implementation as dependencies, and there is an empty Initialize method. The code in Figure 5 shows how to compile the test.
Keep in mind that the dashboard requires all its Dependencies for efficient work. This is why views and services are passed in. I didn't implement the initialization method, so if I run the test, I will get NotImplementedException.
As mentioned above, I did not blindly write the interpreter code. By checking and testing, I have learned how the interpreter should behave after the initialization method is called. The implementation code of the behavior is as follows:
public void Initialize(){task.GetCustomerList().BindTo(view.CustomerList);}
The source code included in this article includes the complete implementation of the GetCustomerList method in the CustomerTask class (implementing the ICustomerTask interface. Although from the perspective of implementation and testing, I do not need to know whether there is a work implementation. However, this abstraction level makes it difficult for me to pass the testing of the delegate class. The first test is now in the state of compiling and running. This proves that when the Initialize method on the provisioner is called, it will interact with the dependent object in the way I specified in the test, finally, when the specific implementation of these dependent objects is inserted into the dashboard, I can be sure that the result view (ASPX page) will be filled by the customer list.
Back to Top
Fill in DropDownList
So far, I have mainly processed interfaces and put aside the actual implementation details, so I will focus on the dashboard. Now it's time to create some probe code that will eventually allow the probe to populate the list on the Web page in a testable manner. The key to implementing this function is the interaction in the BindTo method of the LookupCollection class. If you look at the implementation of the LookupCollection class in section 6, you will notice that it implements the ILookupCollection interface. The source code in this article is accompanied by a test and can be used to create the LookupCollection class function.
The implementation of the BindTo method is particularly interesting. Note that in this method, the set will repeat ILookupDTO to implement its own private list. ILookupDTO is an interface that can be well bound to the combo box on the UI Layer:
public interface ILookupDTO{string Value { get; }string Text { get; }}
Figure 7 shows the code used to test the BindTo method of the lookup set. This method will help explain the expected interaction between LookupCollection and ILookupList. The last point is particularly interesting. In this test, I want to call the Clear method in the ILookupList implementation before trying to add a project to the list. Then, I want to call Add 10 times on ILookupList. As a parameter of the Add method, LookupCollection will be passed in the object implementing the ILookupDTO interface. To work with controls in a Web project (such as a drop-down list box), you need to create an ILookupList implementation that knows how to work with controls in a Web project.
The source code included in this article contains a project named MVP. Web. Controls. This project contains all the Web-specific controls or classes I have selected for creating the complete solution. Why do I put the code in this project instead of in the APP_CODE directory or Web project? The answer is testability. It is difficult to directly test any controls in a Web project without manually running an application or using a test program to automatically execute a UI test. The MVP mode allows me to consider higher abstraction levels without having to manually run the application and test the implementation of core interfaces (ILookupList and ILookupCollection. I plan to add a new class: WebLookupList control to the Web. Controls project. Figure 8 shows the first test of this class.
Some items are highlighted in the test shown in figure 8. Obviously, the test project requires a reference to the System. Web library so that it can instantiate the DropDownList Web control. For further test, you should understand that the WebLookupList class will implement the ILookupList interface. It also uses ListControl as a dependent object. The two most common ListControl implementations in the System. Web. UI. WebControls namespace are the DropDownList and ListBox classes. The main function tested in Figure 8 is to ensure that WebLookupList correctly updates the status of the actual Web ListControl to the status in which it is delegated responsibility. Figure 9 shows the class charts involved in the WebLookupList implementation. I can use the code shown in Figure 10 to meet the requirements for the first test of the WebLookupList control.
Figure 9 WebLookupList class
Keep in mind that the key of MVP is the separation of layers introduced by the creation of view interfaces. The viewer does not understand the specific implementation of the view and the ILookupList that it wants to talk to. It only knows that it can call any method defined by these interfaces. Finally, the WebLookupList class is a class encapsulated and delegated to the underlying ListControl (some base classes of ListControls defined in the System. Web. UI. WebControls project ). With this code, I can compile and run the WebLookupList control test. Now the test should pass smoothly. I can add another test for WebLookupList to test the actual behavior 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);}
In addition, I will test whether the underlying ListControl (DropDownList) status will be changed when the WebLookupList class's own method is called. WebLookupList can now complete the DropDownList function in the Web Form. Now you can bind all the programs together to get the Web page drop-down list that has been filled in the customer list.
Back to Top
View Interface implementation
Because I am creating a Web Form front-end, the Implementation Program of the IViewCustomerView interface must be a Web Form or user control. For this reason, I set it to Web Form. The general appearance of the page has been created, as shown in figure 2. Now I only need to implement the View Interface. Switch to the source code of the ViewCustomers. aspx page. You can add the following code to implement the IViewCustomersView interface on this page:
public partial class ViewCustomers :Page,IViewCustomerView
If you observe the sample code, you will find that the Web project and Presentation are two completely different sets. In addition, the Presentation project does not reference any Web. UI project, which further maintains the separation layer. On the other hand, the Web. UI project must reference the Presentation project because both the View Interface and provisioner are in the project.
By selecting to implement the IViewCustomerView interface, our Web page can now implement any method or attribute defined by this interface. Currently, the IViewCustomerView interface has only one attribute. It is a getter that can return any implementation of the ILookupList interface. I have added a reference to the Web. Controls project to instantiate WebLookupListControl. This is because WebLookupListControl implements the ILookupList interface and knows how to delegate it to the actual WebControls in ASP. NET. View the aspx on the ViewCustomer page. You will find that the customer list is just an asp: DropDownList control:
<td>Customers:</td><td><asp:DropDownList id="customerDropDownList" AutoPostBack="true"runat="server" Width="308px"></asp:DropDownList></td></tr>
Using the existing code, I can quickly continue to implement the Code required to implement the IViewCustomerView interface:
public ILookupList CustomerList{get { return new WebLookupList(this.customerDropDownList);}}
Now I need to call the Initialize method on the provisioner to trigger this method to actually execute some operations. Therefore, a view needs to be able to instantiate the dashboard so that its method can be called. If you look back at the dashboard, you will remember that it needs to be used with the view and service. The ICustomerTask interface indicates the interface at the application service layer. The service layer is usually responsible for coordinating the interaction between domain Objects and converting the interaction results into "Data Transmission Objects" (DTO ), then it is passed from the service layer to the presentation layer and then to the UI Layer. However, there is a problem here: I have stipulated that the dashboard should be constructed together with the view and Service implementation.
The actual instantiation of the tool will be carried out in the source code of the Web page. This is a problem because the UI project does not reference any service layer project. However, the project references the service layer project. You can solve this problem by adding an overloaded constructor to ViewCustomerPresenterClass:
public ViewCustomerPresenter(IViewCustomerView view) :this(view, new CustomerTask()) {}
This new constructor meets the requirements for the presentation view and service implementation, and maintains the separation of the UI Layer from the service layer. Now the subsequent code of the source code is simple:
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();}
Note that the key to indicating the Object Instantiation is: I will use the newly created constructor to overload it, and the Web Form will pass itself as an object for implementing the View Interface.
Using the code in the implemented source code, I can immediately create and run the application. You can use the customer name list to fill in the DropDownList on the Web page without any data binding code in the source code. In addition, test scores are run on all code segments that ultimately work together, ensuring that the presentation layer architecture will run as expected.
Now I want to demonstrate the steps required to display the selected customer information in DropDownList to summarize my MVP discussion. I reiterate that I will first write a test to describe what I want to observe. (See figure 11 ).
As mentioned above, I will use the NMock library to create task and view interface simulation. This specific test verifies the behavior of the provisioner by sending a request to the service layer to represent the DTO of a specific customer. After the DTO is retrieved from the service layer, it directly updates the attributes of the view, so that the view does not have to understand any knowledge about how to correctly Display object information. For simplicity, I will not discuss the implementation of the SelectedItem attribute on the WebLookupList control; instead, I will leave it to you to check the source code for detailed implementation information. This test shows the interaction between the tool and the view after the merdto operator is retrieved from the service layer. If I try to run the test now, I will face a serious failure because many attributes on The View Interface do not exist. Therefore, I will continue and add necessary members for the IViewCustomerView interface, as shown in 12.
After these interface members are added, my Web Form may complain because it no longer meets the interface protocol, so I have to return the source code of the Web Form and implement other members. As described above, the entire tag of the Web page has been created, and the table cell has been marked as "runat = server", and has been named based on the information it should display. In this way, the result code can easily implement interface members:
public string CompanyName{set { this.companyNameLabel.InnerText = value; }}public string ContactName{set { this.contactNameLabel.InnerText = value; }}...
With the implementation of the setter attribute, only the last thing is to be done now. I need a way to tell the dashboard to display the information of the selected customer. Looking back at the test, you will find that the implementation of this behavior is in the displaymermerdetails method of the dashboard. However, this method does not contain any parameters. When the call is made, the tool returns to the view, extracts any information it needs (retrieved using ILookupList), and then uses this information to retrieve the details of the selected customer. From the UI perspective, what I need to do is to set the AutoPostBack attribute of DropDownList to true. I also need to add the following event handler hook code to the OnInit method on 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 when a new customer is selected from the drop-down list, the view displays the details of the customer in the request viewer.
It is important to note that this is a typical behavior. When a view request is sent to the operator, it does not give any specific details and determines whether to return the view, you can use the View Interface to obtain any information you need. Figure 13 shows the code that represents the behavior required by the machine.
I hope you can now understand the value of adding a representation layer. The caller is responsible for searching the customer ID whose details need to be displayed. This is the code that is usually executed in the source code, but it is now in the class, and I can fully test and practice it outside of any presentation layer technology.
If the caller is able to retrieve valid customer IDs from the view, it will switch to the service layer and request the DTO that represents the customer details. After obtaining DTO, The DTO will update the view with the information contained in DTO. The key point to note is the simplicity of the View Interface. Besides the ILookupList interface, the view interface is completely composed of the string PES ypes. The final responsibility of the delegate is to correctly convert and format the information retrieved from DTO so that it can be used as a string and actually transmitted to the view. Although not described in this example, the provisioner can also read information from the view and convert it to the necessary type expected by the service layer.
After all the code segments are completed, I can run the application now. When loading the page for the first time, I will get a list of customers and display (unselected) the first customer in the DropDownList. If I select a customer, a message is sent back, the view interacts with the dashboard, and the Web page is updated with the relevant customer information.
Back to Top
Future Plan
The Model View Presenter design mode is actually the latest version of the template View Controller that many developers are already familiar; the main difference between the two is that MVP truly isolates the UI from the application's domain/service layer. Although this example is quite simple from the requirement perspective, it can help you abstract the interaction between the UI and other layers of the application. Additionally, you can now learn a variety of ways: You can indirectly use these layers to automatically test your applications. With your in-depth research on the MVP mode, I hope you can find other methods to extract more formats and conditional logic from the source code, and place it in the testable view/Presenter interaction model.
Please send your questions and comments to mmpatt@microsoft.com.
Jean-Paul Boodhoo is a senior. NET delivery expert at ThoughtWorks and has participated in many enterprise-level application delivery using the. NET Framework and various flexible methods. He often uses test-driven development to provide demos on using the. NET function. You can contact Jean-Paul via mailtio: bitwisejp@gmail.com or www.jpboodhoo.com/blog.
This article is excerpted from MSDN Magazine's August 2006.