首先简单说一下MVP,对于一个UI模块来说,它的所有功能被分割为三个部分,分别通过Model、View和Presenter来承载。Model、View和Presenter相互协作,完成对最初数据的呈现和对用户操作的响应,它们具有各自的职责划分。Model可以看成是模块的业务逻辑和数据的提供者;View专门负责数据可视化的呈现,和用户交互事件的相对应。一般地,View会实现一个相应的接口;Presenter是一般充当Model和View的纽带。在MVP中,应用程序的逻辑主要在Presenter来实现,View是很薄的一层,能够把信息显示清楚即可,View只应该有简单的Set/Get的方法,用户输入和设置界面显示的内容,除此就不应该有更多的内容。
下面就一个简单的例子来看看具体的MVP模式是如何实现的:
假设我们要实现如下功能:
从图中可以看到该模块的功能很简单,用户添加新的User点击Save后系统保存并刷新上方列表。
首先隔离功能,构建两个用户控件UserAdd和UserList
这样分开两个用户控件是为了实现功能的分离UserAdd只负责收集用户输入,UserList只负责显示数据列表
接着我们看看UserAdd的代码实现:
从代码中可以看到UserAdd的功能非常简单、单一,只负责显示UserName和UserAge,接受了用户的Click事件也不处理而是公开了一个UserAddEvent让外部去实现。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
public partial class UserAdd : UserControl, IUserAdd { public event EventHandler UserAddEvent; public string UserName { set { this.txbName.Text = value; } get { return this.txbName.Text; } } public string UserAge { set { this.txbAge.Text = value; } get { return this.txbAge.Text; } } public UserAdd() { InitializeComponent(); } private void btnAdd_Click(object sender, EventArgs e) { if (UserAddEvent != null) UserAddEvent(this, e); } }
代码中可以看到,UserAdd实现了一个接口IUserAdd
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public interface IUserAdd 2 { 3 event EventHandler UserAddEvent; 4 5 string UserName { get; set; } 6 7 string UserAge { get; set; } 8 9 }
抽象出一个接口是为了,以后Presenter依赖抽象即依赖接口而不是依赖特定的UserAdd类。
然后对UserList也可以进行相似的抽象实现。
Model用于维护User列表信息,下面来看一下Model的实现:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public class UserModel:IUser 2 { 3 private readonly IList<User> _users = new List<User>(); 4 5 public UserModel() 6 { 7 // generate some test data 8 AddItem(new User { Name = "Peter", Age = 29 }); 9 } 10 11 /// <summary> 12 /// Return data property cast to proper type 13 /// </summary> 14 public IList<User> Users 15 { 16 get { return _users; } 17 } 18 19 /// <summary> 20 /// add an item to the data 21 /// </summary> 22 /// <param name="user"></param> 23 public void AddItem(User user) 24 { 25 Users.Add(user); 26 } 27 28 /// <summary> 29 /// update an item in the data 30 /// </summary> 31 /// <param name="user"></param> 32 public void UpdateItem(User user) 33 { 34 for (int i = 0; i < Users.Count; i++) 35 { 36 if (Users[i].Name.Equals(user.Name)) 37 { 38 Users[i] = user; 39 break; 40 } 41 } 42 } 43 44 /// <summary> 45 /// delete an item in the data 46 /// </summary> 47 /// <param name="user"></param> 48 public void DeleteItem(User user) 49 { 50 for (int i = 0; i < Users.Count; i++) 51 { 52 if (Users[i].Name.Equals(user.Name)) 53 { 54 Users.RemoveAt(i); 55 break; 56 } 57 } 58 } 59 }
我们也为UserModel抽象出一个接口:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public interface IUser 2 { 3 IList<User> Users { get; } 4 void AddItem(User user); 5 void UpdateItem(User user); 6 void DeleteItem(User user); 7 }
这样做的目的也是为了使Presenter依赖于接口而不是依赖于UserModel类。
好了View和Model都实现好了,下面要实现的是最关键的Presenter了,需要通过Presenter将View和Model联系起来。
先来看UserAddPresenter的实现:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public class UserAddPresenter:IPresenter 2 { 3 public IUserAdd _userAdd; 4 public IUserModel _userModel; 5 public UserAddPresenter(IUserAdd userAdd, IUserModel userModel) 6 { 7 this._userAdd = userAdd; 8 this._userModel = userModel; 9 this._userAdd.UserAddEvent += UserAddResponse; 10 //初始化时将此Presenter加入到MessageCenter的PresenterList上 11 MessageCenter.SinlgeInstance.AddPresenter(this); 12 } 13 public void UserAddResponse(object sender, EventArgs e) 14 { 15 _userModel.AddItem( 16 new User() 17 { 18 Name = _userAdd.UserName, Age = Convert.ToInt32(_userAdd.UserAge) 19 }); 20 SendMessage(OperationType.Add); 21 } 22 public void SendMessage(OperationType operationType) 23 { 24 25 MessageCenter.SinlgeInstance.CallPresenters(operationType); 26 27 } 28 public void ResponseCall(OperationType operationType) 29 { 30 31 } 32 }
代码中可以看到UserAddPresenter中将IUserAdd,IUserModel联系起来。
this._userAdd.UserAddEvent += UserAddResponse;
此Presenter中实现了IUserAdd.UserAddEvent也就是说UserAdd接收到了客户的按钮点击 具体的逻辑实现UserAdd就不管了,而是在此交给了UserAddPresenter来处理。
可以看得到UserAddPresenter首先调用了Model将数据保存了起来,随后调用SendMessage发送了一条消息。
现在我们想要用户点击Save时保存用户输入的数据这一步已经实现,如何能实现刷新用户列表呢?我们肯定要有个地方通知到UserList。
我们调用了MessageCenter.SinlgeInstance.CallPresenters那下面我们看看MessageCenter中做了什么。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public class MessageCenter 2 { 3 private MessageCenter() 4 { 5 PresentersList = new List<IPresenter>(); 6 } 7 private static MessageCenter instance; 8 private static object lockObj=new object(); 9 public static MessageCenter SinlgeInstance 10 { 11 get 12 { 13 if (instance == null) 14 { 15 lock (lockObj) 16 { 17 if (instance == null) 18 { 19 instance = new MessageCenter(); 20 } 21 } 22 } 23 return instance; 24 } 25 } 26 protected IList<IPresenter> PresentersList 27 { 28 get; 29 set; 30 } 31 public void AddPresenter(IPresenter presenter) 32 { 33 PresentersList.Add(presenter); 34 35 } 36 public void RemovePresenter(IPresenter presenter) 37 { 38 PresentersList.Remove(presenter); 39 } 40 public void CallPresenters(OperationType operationType) 41 { 42 foreach (IPresenter presenter in PresentersList) 43 presenter.ResponseCall(operationType); 44 } 45 }
可以看出来MessageCenter中维护了一个PresentersList,当有消息发来时MessageCenter就会遍历PresentersList发送消息,谁需要对这个消息进行处理即自行处理,不需要的话直接忽略即可。
这有点类似于,员工小王用OutLook给公司所有人发了一封关于设计模式培训的邮件,他并不知道"所有人"里都包含哪些人,他只需要在收件人里写上AllUsers,点发送即可具体谁会收到谁想处理已经和小王无关,消息中心收到消息后,会遍历公司所有人并将邮件发给他们,所有人都会收到邮件,然而具体怎么处理是他们自己的事,测试人员可能会直接忽略这封邮件,而开发或设计人员可能会在日程上进行标记以便能准时参加培训。
来看看UserListPresenter的实现吧:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public class UserListPresenter : IPresenter 2 { 3 public IUserModel _userModel; 4 public IUserList _userList; 5 public UserListPresenter(IUserList userList, IUserModel userModel) 6 { 7 this._userList = userList; 8 this._userModel = userModel; 9 this._userList.StartLoadEvent += UserListLoad; 10 MessageCenter.SinlgeInstance.AddPresenter(this); 11 } 12 public void UserListLoad(object sender, EventArgs e) 13 { 14 _userList.Users = _userModel.Users; 15 } 16 public void SendMessage(OperationType operationType) 17 { 18 MessageCenter.SinlgeInstance.CallPresenters(operationType); 19 } 20 public void ResponseCall(OperationType operationType) 21 { 22 switch (operationType) 23 { 24 case OperationType.Add: 25 _userList.Users = _userModel.Users; 26 break; 27 default: 28 break; 29 } 30 } 31 }
可以看到ResponseCall方法中对Add消息进行了处理即刷新列表。 每个Presenter都需要有一个ResponseCall方法,所以抽象出接口IPresenter
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 public interface IPresenter 2 { 3 void ResponseCall(OperationType operationType); 4 }
这样的话在MessageCenter就可以遍历调用IPresenter的ResponseCall方法了。
这样做的好处是,在设计每个Presenter的时候,不必关心它以后会被谁调用,只需要处理好自己的逻辑以及对感兴趣的消息的处理。
至此,这个简单的示例就讲解完毕了。
总结一下MVP,Model负责数据的维护,View只负责显示数据,Presenter就是将View和Model联系起来,而Presenter之间的交互也不是直接交互而是通过MessageCenter进行联系。
采用MVP模式实现的项目很容易进行单元测试,因为处理逻辑主要集中在Presenter中,而Presenter与Model和View的关联也都是通过接口,这样就可以在Model和View都没有完成的情况下对Presenter进行单元测试。
本人初识此模式,在此也只是将自己的一些培训所得和肤浅总结记录一下而已,纰漏之处欢迎各位高手指正。