As a software developer, I often think how am I able to test my User Interface. There are many business logic that I really what to test, yet, they are embedded with the user interface class which makes it very hard to write automated test.
From one of my old project, I came across this design pattern call Model View Presenter. It is a derivative of Model View Controller. There a number of articles regarding this design pattern. What I decided to do is put my own explanation on the blog, which reinforce my understand of MVP and hope helps others to understand.
What is MVP?
Typical UI - Data Model
[UI Control (i.e. DataGrid) ] ----> [Data Model (i.e. Database Model)]
Basic Model View Presenter
[UI Control] <-----> Presenter ----->[Data Model]
As you can see, there is a Presenter Layer in MVP. This layer separate the business logic from your UI Controller. The Presenter will handle the business logic and your UI will focus on the user interaction.
The Goals of MVP
- Reduce complexity in a UI by extracting the business logic
- Help developer write automated unit test for your business logic
- Increase business logic reusable for other UI Controls.
Ask your application grow, your UI complexity increases as time goes. It is a good idea to extract the business logic out so that your UI can focus on the user interactions.
Since the Presenter is not a UI, you can write unit tests for your Presenter class, instead of clicking your UI.
Once you have your Presenter, you can create other UI Controls that uses the same Presenters.
For example:
[UI Control One (e.g. DataGrid)] <------------> [Presenter] ---> [Data Model]
[UI Control Two (e.g. custom ListView]<---------^
So, if one day you don't like your old UI Control, you can create a new one without worrying about the business logic by using the Presenter. Or, if your application allows different view of the same data, you can have multiple UI that uses the Presenter.
MVP Setup
UIControl ----> IView <----Presenter ----> Data Model
| ^
|-------------------------------|
IView - an interface which state the contract that what the UIControl needs to have in order to support the Presenter
UIControl - your typical user interface, that must inherit from IView, so that the Presenter can control this UIControl. It should contain no or minimum business logic if possible.
Presenter - the class that can manipulate class that inherits IView . It should contain all the business logic allow us to write tests on it.
Data Model - your database access, file access class, or service access. The idea is to have your Presenter deal with your Data Model, and not your UIControl
MVP Example
I have to apologize that I didn't upload my solution since I just got a new ISP, and I didn't have time to find out what it has to offer! so I will use code snippet
This is a very simple Feedback Controller.
- Interface IUserFeedBack is our IView
- MyFeedBackControl is our UIControl
- UserFeedBackPresenter is our Presenter
IUserFeedBack - this specify what the presenter can do with your UIController. As you can see, this a very basic method that a typical UI will do. However, instead of letting your UI decided when these actions are to be done, your presenter will decide, and your UI just need to do what the function name suggested
1: using System;
2:
3: namespace MVPExample
4: {
5: /// <summary>
6: /// Any control that support IUserFeedBack can work with the UserFeedBackPresenter
7: /// </summary>
8: public interface IUserFeedBack
9: {
10: void ClearUserInput();
11: void DisplayMessage(String savedMessage);
12: void PopulateFeedBackType(String[] data);
13: void PopulateItemList(String[] data);
14: void SetDefaultFeedBackType(int defaultFeedBackTypeIndex);
15: void SetDefaultSelectedOption(int[] defaultSelectedOptions);
16: }
17: }
UserFeedBackPresenter - please excuse my poor English spelling and grammar. I have a very simple Presenter which has two important methods: Submit, and Cancel. The Submit() function is called when user submit their feed back though the UI Controller. Once submit is completed, the function will display a message and clear user input. These are the business logic that you may need to do in your presenter, and not your UI Control.
Important: If you look at the constructor, you have to pass a IUserFeedBack object.
1: using System;
2: using System.Collections.Generic;
3:
4: using MVPExample;
5:
6: namespace MVPExample
7: {
8: /// <summary>
9: /// The presenter is the link between the IUserFeedBack control and your data model
10: /// </summary>
11: public class UserFeedBackPresenter
12: {
13: IUserFeedBack userFeedbackControl = null;
14: List<String> feedBackTypeList = new List<string>();
15: List<String> itemList = new List<string>();
16:
17: public UserFeedBackPresenter(IUserFeedBack someControl)
18: {
19: userFeedbackControl = someControl;
20:
21: feedBackTypeList.Add("Satisfied");
22: feedBackTypeList.Add("Alright");
23: feedBackTypeList.Add("Unsatisfied");
24: feedBackTypeList.Add("No Idea");
25:
26: userFeedbackControl.PopulateFeedBackType(feedBackTypeList.ToArray());
27: userFeedbackControl.SetDefaultFeedBackType(0);
28:
29: itemList.Add("Customer Service");
30: itemList.Add("Product Functionalities");
31: itemList.Add("User Interface");
32: itemList.Add("Software Defects");
33:
34: userFeedbackControl.PopulateItemList(itemList.ToArray());
35: userFeedbackControl.SetDefaultSelectedOption(new int[] {1,2});
36:
37: }
38:
39: public void Submit(string name, string email, string selectedType, string[] selectedOptionList)
40: {
41: string message = String.Format("Thank you for submitting the following information\nName: {0}\nEmail: {1}\nFeedBack: {2}\nSelected Option:{3}",
42: name, email, selectedType, String.Join(",", selectedOptionList));
43:
44: //
45: //Save all this information to database, or write stuff to your files
46: //
47: //This is where you can access your data model though the presenter, and not the
48: //User Interface
49: //
50:
51: //Display message to the user
52: userFeedbackControl.DisplayMessage(message);
53: userFeedbackControl.ClearUserInput();
54:
55: }
56:
57: public void Cancel()
58: {
59: userFeedbackControl.ClearUserInput();
60: userFeedbackControl.DisplayMessage("You have canceled your feedback");
61: }
62: }
63: }
MyFeedBackControl - this control is derived from UserControl, and IUserFeedBack. It has a combobox that display the type of feedback a user can submit, 2 text boxes that store user name and email, and a check list box allow user to select what this feedback is related. When user click on the submit, or cancel button, the UI Control doesn't care what the business logic need to do, it lets the presenter deal with it.
Important: If you take a look at the constructor, you see this important part
userFeedBackPresenter = new UserFeedBackPresenter(this);
This passes the control to the presenter, so that presenter can interact with your UIControl. If we go further from this example, we can have our presenter dictates what field needs to be create during runtime, and the UIControl will only care how to create or what control need to create.
1: using System;
2: using System.Collections.Generic;
3: using System.Windows.Forms;
4:
5: namespace MVPExample
6: {
7: public partial class MyFeedBackControl : UserControl, IUserFeedBack
8: {
9: //The presenter allows the separation between the UI control and the data model
10: private UserFeedBackPresenter userFeedBackPresenter;
11:
12: #region constructor
13:
14: public MyFeedBackControl()
15: {
16: InitializeComponent();
17: //passing the control to the presenter
18: userFeedBackPresenter = new UserFeedBackPresenter(this);
19: FeedBackOptionListBox.CheckOnClick = true;
20: }
21:
22: #endregion
23:
24: #region event handlers
25:
26: private void btnCancel_Click(object sender, EventArgs e)
27: {
28: userFeedBackPresenter.Cancel();
29: }
30:
31: private void btnSubmit_Click(object sender, EventArgs e)
32: {
33: List<string> selectedOptionList = new List<string>();
34: foreach (object item in FeedBackOptionListBox.CheckedItems)
35: {
36: selectedOptionList.Add(item.ToString());
37: }
38:
39: userFeedBackPresenter.Submit(
40: this.NameTextBox.Text,
41: this.EmailTextBox.Text,
42: this.FeedBackTypeComboBox.SelectedItem.ToString(),
43: selectedOptionList.ToArray());
44: }
45:
46: #endregion
47:
48: #region IUserFeedBack Members
49:
50: /// <summary>
51: /// Clears the user input on this control.
52: /// </summary>
53: public void ClearUserInput()
54: {
55: FeedBackOptionListBox.ClearSelected();
56:
57: for (int i = 0; i < FeedBackOptionListBox.Items.Count; i++)
58: {
59: FeedBackOptionListBox.SetItemChecked(i, false);
60: }
61:
62: FeedBackTypeComboBox.SelectedItem = FeedBackTypeComboBox.Items[0];
63: NameTextBox.Clear();
64: EmailTextBox.Clear();
65: }
66:
67: public void DisplayMessage(string message)
68: {
69: MessageBox.Show(message);
70: }
71:
72: public void PopulateFeedBackType(string[] data)
73: {
74: //Clear all item in combobox
75: this.FeedBackTypeComboBox.Items.Clear();
76:
77: //populate our combobox with Feedback types
78: foreach (string item in data)
79: {
80: this.FeedBackTypeComboBox.Items.Add(item);
81: }
82: }
83:
84: public void PopulateItemList(string[] data)
85: {
86: this.FeedBackOptionListBox.Items.Clear();
87:
88: foreach (string item in data)
89: {
90: this.FeedBackOptionListBox.Items.Add(item);
91: }
92: }
93:
94: public void SetDefaultFeedBackType(int defaultFeedBackTypeIndex)
95: {
96: this.FeedBackTypeComboBox.SelectedItem = this.FeedBackTypeComboBox.Items[defaultFeedBackTypeIndex];
97: }
98:
99: public void SetDefaultSelectedOption(int[] defaultSelectedOptions)
100: {
101: this.FeedBackOptionListBox.ClearSelected();
102:
103: foreach (int index in defaultSelectedOptions)
104: {
105: this.FeedBackOptionListBox.SetItemChecked(index, true);
106: }
107: }
108:
109: #endregion
110: }
111: }
Conclusion
As I remember someone has said from the Microsoft, they help the developers to write software by providing abstract layer so that developer don't have to worry about the back end stuff. MVP is similar in a sense that the UIControl doesn't need to care what the presenter does behind. You can have 2 developers in a team. One of them only care about what need the UI have to do (Presenter), and the other only care how the UI display and interact with the user (UIControl). As long as they agree on the IView, everything should work!
Where is the Unit Test?
Yea, I am not a big fan of test driven programming methodology YET! I will write another post about testing using mock framework, specifically RhinoMock in the near future. Then I will continue with this example.