最近,我一直在重構之前做的一個項目,在這個過程中感慨萬千。原先的項目是一個運用了WCF的C/S系統,在客戶端運用了MVC模式,但MVC的View、Model耦合以及WCF端分布式欠佳等問題讓我有了重構的想法,經過了一段時間的改造,逐漸形成了MVP+三層結構+WCF的面向服務的程序架構。在這里我把我的想法寫成了一個例子,供大家參考。
在正式開始講解之前,我必須得感謝Artech、代震軍等諸多大蝦,他們的文章給了我很大的啟發。
我寫的這個例子是關於博客管理的,邏輯很簡單,就是用戶發表文章、發表評論,管理員可以對用戶進行管理。讓我們先從MVP的運用開始講起。
MVP與MVC的選擇
關於MVP和MVC,我只談談在重構過程中的看法。在經典的MVC中,View會通過Controller調用Model中的方法,Model被更新后會立即通知View,因此View與Model還是存在一定程度的耦合的,Controller也只是作為一個簡單的消息分發器,View與Controller也緊緊的貼合好像是一個整體。而在MVP中,Presenter代替Controller而出現,成為View與Model解耦的關鍵,在MVP中,Presenter掌管大權,View的請求提交給Presenter,由Presenter調用Model,Model更新后的狀態再返回給Presenter,由Presenter控制View進行相應的顯示。如此,Model與View被完全解耦,View與Controller也形成了單向依賴。MVC與MVP可以說各有特點,但我更傾向於MVP。
關於MVC和MVP,給大家看這兩張圖就很明白了。對於剛接觸MVP的朋友可能不知道View、Presenter和Model中具體該實現哪些東西,我會在用實例講解的時候告訴大家我的想法。
MVP實戰運用
在確定使用MVP模式之后,我給我的博客程序安排了如下幾個項目:
Main:主程序入口點。
Common:存放委托和公共組件等。
Model:MVP中的M,注意區別於三層中的Model。
Presenter:MVP中的P。
View:MVP中的V。
DTO:這個項目其實和三層中的Model作用是一樣的,但是我為了區別於MVP中的Model,把它叫做DTO,其實它的作用就是一個DTO。
項目之間的引用關系是這樣的:
Main直接調用Presenter啟動程序。Presenter與View,Presenter和Model分別都是單向引用,而View和Model完全沒有任何聯系。View和Presenter需要引用Common,使用其中的一些公共組件,而DTO作為存放數據傳輸對象的項目View、Presenter和Model必須都要引用。
在搭建好項目框架之后,我們可以先從Model入手,因為Model是業務邏輯的所在地,是數據的提供者,它完全基於用例的。按照本例需求,分析出三個實體,分別是User(用戶),Note(文章),Comment(評論),我們就以User為例,寫一個Model。代碼如下所示:

1 /// <summary>
2 /// The interface of user model
3 /// </summary>
4 public interface IUserGroup
5 {
6 #region --Methods--
7 /// <summary>
8 /// Get all users
9 /// </summary>
10 /// <returns>Users</returns>
11 IList<User> GetAllUsers();
12
13 /// <summary>
14 /// Get user by user id
15 /// </summary>
16 /// <param name="id">the user id</param>
17 /// <returns>User</returns>
18 User GetUserById(string id);
19
20 /// <summary>
21 /// Update user
22 /// </summary>
23 /// <param name="user">the user</param>
24 void UpdateUser(User user);
25
26 /// <summary>
27 /// Delete user by user id
28 /// </summary>
29 /// <param name="userId">the user id</param>
30 void DeleteUser(string userId);
31 #endregion
32 }
這是一個用於處理User業務邏輯的接口,其中使用到的User類就是DTO中的User實體。Model中把接口公開出來供Presenter調用就可以了,Note和Comment的代碼也類似,P-M之間的交互還是比較簡單的。
OK,設計完Model,我們再來看看界面如何呈現,我的想法如圖所示:
界面很丑陋,大家就湊合看吧。窗口就一個,有一個DataGridView用來顯示所有用戶,當選中一行時在下面的TextBox中顯示用戶的詳細信息。我們所能看到的這個UI界面就是MVP中View了。設計View時需要注意的是,View一定要針對接口設計,不要針對實現,因為我們的設想是將View做成Passive View,一定要讓Presenter依賴於一個抽象的View。
在本例中,我將這個界面分解為兩個View,一個是顯示用戶詳細信息的主窗體(IUserControlView),一個是顯示所有用戶信息的列表(IGridView),可以看到IGridView是IUserControlView的一部分。代碼如下所示:

1 /// <summary>
2 /// The interface of UserControlView
3 /// </summary>
4 public interface IUserControlView
5 {
6 #region --Event--
7 /// <summary>
8 /// Occurs when the view was loaded
9 /// </summary>
10 event EventHandler OnViewLoad;
11 #endregion
12
13 /// <summary>
14 /// set the UserGridView
15 /// </summary>
16 IGridView UserGridView
17 {
18 set;
19 }
20
21 #region --Methods--
22 /// <summary>
23 /// Initialize the components of this view
24 /// </summary>
25 void Initialize();
26
27 /// <summary>
28 /// Show one user information at the interface
29 /// </summary>
30 /// <param name="user"></param>
31 void ShowUserInfo(User user);
32
33 /// <summary>
34 /// Show alert form
35 /// </summary>
36 /// <param name="message">Messages should be shown</param>
37 void Alert(string message);
38 #endregion
39 }

1 /// <summary>
2 /// The interface of Grid View
3 /// </summary>
4 public interface IGridView
5 {
6 #region --Event--
7 /// <summary>
8 /// Occurs when a user was selected
9 /// </summary>
10 event UserEventHandler OnUserSelected;
11 /// <summary>
12 /// Occurs when a user is begin to be edited
13 /// </summary>
14 event UserEventHandler OnUserBeginEdit;
15 #endregion
16
17 #region --Properties--
18 /// <summary>
19 /// 設置此View相對於父View的位置
20 /// </summary>
21 Point ViewLocation
22 {
23 set;
24 }
25 #endregion
26
27 #region --Methods--
28 /// <summary>
29 /// bind data to this grid
30 /// </summary>
31 void BindData(IList<User> users);
32 #endregion
33 }
實現代碼如下:

1 public class UserGridView : DataGridView, IGridView
2 public class UserDetailForm : Form, IUserControlView
View實現代碼的細節我就不貼了,在文章最后會提供示例的下載鏈接。
需要注意的是,View既然是針對抽象設計,接口中就不能暴露任何UI實現的細節。
現在,再來看看Presenter如何設計,一般來說一個View就有一個相對應的Presenter來控制,Presenter代碼如下所示:

1 /// <summary>
2 /// The interface of GridPresenter
3 /// </summary>
4 public interface IGridPresenter
5 {
6 #region --Event--
7 /// <summary>
8 /// Occurs when a user was selected
9 /// </summary>
10 event UserEventHandler OnUserSelected;
11 /// <summary>
12 /// Occurs when a user is begin to be edited
13 /// </summary>
14 event UserEventHandler OnUserBeginEdit;
15 #endregion
16
17 #region --Properties--
18 /// <summary>
19 /// Get the view
20 /// </summary>
21 IGridView View
22 {
23 get;
24 }
25 #endregion
26
27 #region --Methods--
28 /// <summary>
29 /// Show a group users data
30 /// </summary>
31 /// <param name="users"></param>
32 void ShowUsers(IList<User> users);
33 #endregion
34 }

1 /// <summary>
2 /// The interface of UserControlPresenter
3 /// </summary>
4 public interface IUserControlPresenter
5 {
6 /// <summary>
7 /// Show the mian view.Start the application
8 /// </summary>
9 void Run();
10 }
Presenter對View的引用是單向的,View不知道哪個Presenter在用它,View也無法訪問到Presenter。我們讓Presenter訂閱View中的事件以響應View的請求。View是被動的,需要由Presenter控制,因此在Presenter實例化的時候同時實例化相應的View。
IGridPresenter的實現代碼如下所示:

1 public class UserGridPresenter : IGridPresenter
2 {
3 #region --Event--
4 /// <summary>
5 /// Occurs when a user was selected
6 /// </summary>
7 public event UserEventHandler OnUserSelected;
8 /// <summary>
9 /// Occurs when a user is begin to be edited
10 /// </summary>
11 public event UserEventHandler OnUserBeginEdit;
12 #endregion
13
14 #region --Fields--
15 private IGridView mView;
16 #endregion
17
18 #region --Properties--
19 /// <summary>
20 /// Get the GridView
21 /// </summary>
22 public IGridView View
23 {
24 get { return mView; }
25 }
26 #endregion
27
28 #region --Constructor--
29 /// <summary>
30 /// Default constructor
31 /// </summary>
32 public UserGridPresenter()
33 {
34 mView = new UserGridView();
35 AttachToUserGridView(mView);
36 }
37 #endregion
38
39 #region --Public Methods--
40 /// <summary>
41 /// show user data
42 /// </summary>
43 /// <param name="users"></param>
44 public void ShowUsers(IList<User> users)
45 {
46 mView.BindData(users);
47 }
48 #endregion
49
50 #region --Private Methods--
51 /// <summary>
52 /// Attach to the UserGridView
53 /// </summary>
54 /// <param name="view"></param>
55 private void AttachToUserGridView(IGridView view)
56 {
57 if (view != null)
58 {
59 view.OnUserSelected += new UserEventHandler(UserGridView_OnUserSelected);
60 view.OnUserBeginEdit += new UserEventHandler(UserGridView_OnUserBeginEdit);
61 }
62 }
63 #endregion
64
65 #region --Event Methods--
66 /// <summary>
67 /// Occurs when the OnUserSelected event in UserGridView was raised
68 /// </summary>
69 private void UserGridView_OnUserSelected(object sender, UserEventArgs e)
70 {
71 RaiseOnUserSelected(e.UserValue);
72 }
73
74 /// <summary>
75 /// Occurs when the OnUserBeginEdit event in UserGridView was raised
76 /// </summary>
77 private void UserGridView_OnUserBeginEdit(object sender, UserEventArgs e)
78 {
79 RaiseOnUserBeginEdit(e.UserValue);
80 }
81 #endregion
82
83 #region --Raise Event Methods--
84 /// <summary>
85 /// Raise the OnUserSelected event
86 /// </summary>
87 /// <param name="user"></param>
88 private void RaiseOnUserSelected(User user)
89 {
90 UserEventHandler handler = OnUserSelected;
91 if (handler != null)
92 {
93 UserEventArgs e = new UserEventArgs();
94 e.UserValue = user;
95 handler(this, e);
96 }
97 }
98
99 /// <summary>
100 /// Raise the OnUserBeginEdit event
101 /// </summary>
102 /// <param name="user"></param>
103 private void RaiseOnUserBeginEdit(User user)
104 {
105 UserEventHandler handler = OnUserBeginEdit;
106 if (handler != null)
107 {
108 UserEventArgs e = new UserEventArgs();
109 e.UserValue = user;
110 handler(this, e);
111 }
112 }
113 #endregion
114 }
IUserControlPresenter的實現代碼如下所示:

1 public class UserControlPresenterBase : IUserControlPresenter
2 {
3 #region --Fields--
4 private IGridPresenter mGridPresenter;
5 private IUserControlView mUserControlView;
6 #endregion
7
8 #region --Constructor--
9 /// <summary>
10 /// Default constructor
11 /// </summary>
12 public UserControlPresenterBase()
13 {
14 Initialize();
15 }
16 #endregion
17
18 #region --Public Methods--
19 /// <summary>
20 /// Show the mian view.Start the application
21 /// </summary>
22 public void Run()
23 {
24 ServiceCollection.LoadServer();
25 //Run as main form
26 Application.Run(mUserControlView as UserDetailForm);
27 }
28 #endregion
29
30 #region --Private Methods--
31 /// <summary>
32 /// Initialize this presenter
33 /// </summary>
34 private void Initialize()
35 {
36 mUserControlView = new UserDetailForm();
37 mGridPresenter = new UserGridPresenter();
38 mUserControlView.UserGridView = mGridPresenter.View;
39 //The UI initialize method should be executed until all sub views was assigned
40 mUserControlView.Initialize();
41 AttachToUserControlView(mUserControlView);
42 AttachToUserGridPresenter(mGridPresenter);
43 }
44
45 /// <summary>
46 /// Attach to the UserControlView
47 /// </summary>
48 /// <param name="view"></param>
49 private void AttachToUserControlView(IUserControlView view)
50 {
51 if (view != null)
52 {
53 view.OnViewLoad += new EventHandler(UserControlView_OnViewLoad);
54 }
55 }
56
57 /// <summary>
58 /// Attach to the UserGridPresenter
59 /// </summary>
60 /// <param name="presenter"></param>
61 private void AttachToUserGridPresenter(IGridPresenter presenter)
62 {
63 if (presenter != null)
64 {
65 presenter.OnUserSelected += new UserEventHandler(UserGridPresenter_OnUserSelected);
66 presenter.OnUserBeginEdit += new UserEventHandler(UserGridPresenter_OnUserBeginEdit);
67 }
68 }
69
70 /// <summary>
71 /// Show the view to edit a user
72 /// </summary>
73 /// <param name="user"></param>
74 private void ShowEditUserView(User user)
75 {
76 IEditUserPresenter presenter = new EditUserPresenter();
77 presenter.OnUserEdited += new UserEventHandler(EditUserPresenter_OnUserEdited);
78 presenter.ShowView(user);
79 }
80
81 /// <summary>
82 /// Show all users
83 /// </summary>
84 private void ShowAllUsers()
85 {
86 IWS_Blog blogService = ServiceCollection.BlogService;
87 IList<User> userList = blogService.GetAllUsers();
88 mGridPresenter.ShowUsers(userList);
89 }
90 #endregion
91
92 #region --Event Methods--
93 /// <summary>
94 /// Occurs when the UserControlView was loaded
95 /// </summary>
96 private void UserControlView_OnViewLoad(object sender, EventArgs e)
97 {
98 ShowAllUsers();
99 }
100
101 /// <summary>
102 /// Occurs when the OnUserSelected event in UserGridPresenter was raised
103 /// </summary>
104 private void UserGridPresenter_OnUserSelected(object sender, UserEventArgs e)
105 {
106 mUserControlView.ShowUserInfo(e.UserValue);
107 }
108
109 /// <summary>
110 /// Occurs when the OnUserBeginEdit event in UserGridPresenter was raised
111 /// </summary>
112 private void UserGridPresenter_OnUserBeginEdit(object sender, UserEventArgs e)
113 {
114 ShowEditUserView(e.UserValue);
115 }
116
117 /// <summary>
118 /// Occurs when a user edit finished
119 /// </summary>
120 private void EditUserPresenter_OnUserEdited(object sender, UserEventArgs e)
121 {
122 ShowAllUsers();
123 }
124 #endregion
125 }
初始化加載所有用戶信息的流程是這樣的:當UserControlView加載時,UserControlPresenter隨之響應並調用UserModel中的GetAllUsers方法。UserControlPresenter獲取到數據后,再調用GridPresenter中的ShowUsers方法,將查詢的數據綁定到GridView上,這時便看到了數據。在這個過程中,View提交請求是通過事件響應實現的,只要涉及到與數據相關的請求都必須要提交到Presenter中,由Presenter來決定下一步該做什么,View中則不能包含任何與數據相關的業務邏輯。從這個流程中我們也可以看到Presenter的掌控地位,他屬於指手划腳的那類人,只是在告訴別人需要做什么,但自己卻不會親自動手。
大家可能注意到了,我把UserControlView中的Initialize方法公開在接口中,並沒有在UserControlView構造時執行,這是因為UserControlView依賴於一個抽象的GridView,在構造時GridView還沒有被注入,顯然在這個時候初始化會出現未將對象引用設置到對象的實例的異常。因此我把UserControlView的初始化工作交給Presenter處理,將初始化的時機延后到所有依賴注入完成,而View不會主動執行Initialize。
IUserControlPresenter已經是最頂層的Presenter了,它只需要公開出接口供Main調用啟動程序即可。
我感覺,P-V交互是MVP模式運用的重點,本人水平有限,文中疏漏或講解不到位的地方還請大家諒解。關於MVP,大家可以看看Artech和代震軍的文章,你們會學到很多,還有一篇關於MVP的14條規則的文章也強烈推薦。
這篇文章是關於MVP運用的,下面我會介紹我是如何把WCF服務端加入進來的,以及我對三層結構運用的心得。