對MVP模式的接觸,是我偶然一次在百度上搜MVC的時候開始,當時對MVC都不了解,甭說MVP了。后來MVC弄懂了,現在就來了解一下MVP。
MVP 是從經典的模式MVC演變而來的,難怪看那個結構圖有點相像。
MVC模式的結構圖,M,V,C各代表什么不說了
MVP模式的結構圖,M和V的含義跟MVC中的結構一樣,區別的就是C(Controller)和P(Presenter)。感覺這個區別就導致了模式產生性質的變化。至少從幾何角度來看,由一個穩定的三角型變成一條直線。在MVC中即使在Controller對View和Model的控制之下,View和Model之間仍然有聯系,至少View上控件綁定的數據是與Model的某個字段有關的。不過在MVP中Presenter則把原本MVC中View與Model的聯系砍斷了,View上面那個控件綁定什么數據它本身不知道,Presenter才知道。這樣View只是負責呈現部分,使得它的職責更單一了。再者Presenter不是調用View本身,而是調用一個由View實現的接口,這樣使得View與Presenter的聯系更松散了。這么說來,整個MVP模式中的成員一共有四個
- View(視圖):實現IVew接口,負責界面呈現。
- IView(視圖接口):提供一些方法,屬性供展示器調用獲取,從而得知視圖的狀態或某些信息或對視圖進行某些操作,同時也外放了一些方法供展示器注冊,使得視圖能在需要的時刻對展示器發出某些請求。
- Presenter(展示器):整個MVP模式的核心,負責對視圖的操作,數據的綁定,必要時響應來自視圖的請求,在有需要的時候會借助模型完成一些業務。
- Model(模型):完成整個模式中必要的業務邏輯。
瀏覽了一些園友的博文后,我也嘗試實現了一個MVP模式。項目的結構如下圖。
從上圖可以很明顯的看出MVP的三部分,另外Common目錄下存放的主要是MVP模式里的一些基類,接口等等,本項目還使用了一個輕量級的Ioc框架Ninject,為了盡量改動Common里的類,使用Ninject時要綁定的接口是實現類以配置的形式來實現,配置的信息就存放在BindingConfig.xml文件里面。
看一下Common里面包含的類
IocContainer.cs |
Ioc的容器 |
IView.cs |
視圖接口的基接口 |
MyEventArgs.cs |
擴展了事件和委托的參數 |
PresenterBase.cs |
所有展示器的基類 |
PresenterManager.cs |
通過展示器展示其視圖 |
WinFormInjectModule .cs |
Ioc的接口與實現類的綁定 |
由於對Ninject還不是很熟悉,對它的用法解釋不了太多
IocContainer的定義如下
1 public class IocContainer 2 { 3 private static IKernel _kernel; 4 5 public static IKernel Container 6 { 7 get 8 { 9 if (_kernel == null) 10 _kernel = new StandardKernel(new WinFormInjectModule()); 11 return _kernel; 12 } 13 } 14 }
這里用到了WinFormInjectModule類,它繼承了NinjectModule,里面就重寫了Load方法實現綁定,由於這里的綁定時通過配置實現的,所以這里還涉及到讀取和分析配置信息
1 public class WinFormInjectModule : Ninject.Modules.NinjectModule 2 { 3 public override void Load() 4 { 5 6 List<Tuple<string, string>> bindingList = GetBindingConfig(); 7 Type bindType,toType; 8 foreach (Tuple<string,string> item in bindingList) 9 { 10 bindType=Type.GetType(item.Item1); 11 if (item.Item2.Length == 0) 12 { 13 Bind(bindType).ToSelf(); 14 continue; 15 } 16 toType = Type.GetType(item.Item2); 17 Bind(bindType).To(toType); 18 } 19 } 20 21 private List<Tuple<string, string>> GetBindingConfig() 22 { 23 List<Tuple<string, string>> result = new List<Tuple<string, string>>(); 24 XmlDocument xmlDoc = new XmlDocument(); 25 if (!File.Exists("BindingConfig.xml")) throw new IOException("BindingConfig.xml 不存在"); 26 xmlDoc.Load("BindingConfig.xml"); 27 XmlNodeList nodelist = xmlDoc.SelectNodes("//BindingSetting/Binding"); 28 string bind,to; 29 foreach (XmlNode node in nodelist) 30 { 31 bind=string.Empty; 32 to=string.Empty; 33 bind = node.Attributes["bind"].Value; 34 if (node.Attributes["to"] != null) to = node.Attributes["to"].Value; 35 result.Add(new Tuple<string, string>(bind,to)); 36 } 37 return result; 38 } 39 }
配置的定義如下
1 <BindingSetting> 2 <Binding bind="TestMVP.Model.IUser" to="TestMVP.Model.UserModel"/> 3 <Binding bind="TestMVP.View.ILoginView" to="TestMVP.View.LoginView"/> 4 <Binding bind="TestMVP.Presenter.LoginPresenter"/> 5 </BindingSetting>
bind屬性就是要綁定的類或者接口,to就是綁定到的類,如果只是綁定自己的話就在bind屬性填類名則可,to不用填了。
展示器的基類定義如下
1 public class PresenterBase<T> where T : IView 2 { 3 private T _view; 4 5 public PresenterBase(T view) 6 { 7 this.View = view; 8 } 9 10 public T View 11 { 12 get { return _view; } 13 set { _view = value; } 14 } 15 }
以接口的形式對視圖進行訪問的話,就可以避免直接訪問視圖的實例,減少了對視圖的依賴。
考慮到在展示器里打開別的展示器管理的視圖時,原本可以構造一個展示器實例,然后獲取其視圖進行展示,可是在一個展示器里構造另一個展示器,這樣的做法好像不妥,於是定義了一個類專門用於打開別的視圖用的。
當要打開某個視圖(也就是窗體)時,就可以調用PresenterManager的靜態方法
1 public class PresenterManager 2 { 3 public static void ShowView(string presenterName,FormAction formAction) 4 { 5 Type type = Type.GetType("TestMVP.Presenter." + presenterName); 6 object p = Common.IocContainer.Container.GetService(type); 7 System.Windows.Forms.Form frm = type.GetProperty("View").GetValue(p, null) as System.Windows.Forms.Form; 8 switch (formAction) 9 { 10 case FormAction.Run: System.Windows.Forms.Application.Run(frm); 11 break; 12 case FormAction.Show: frm.Show(); 13 break; 14 case FormAction.ShowDialog: frm.ShowDialog(); 15 break; 16 default: 17 break; 18 } 19 } 20 21 public static void ShowView(string presenterName) 22 { 23 ShowView(presenterName, FormAction.Show); 24 } 25 } 26 27 public enum FormAction { Run,Show,ShowDialog }
下面則做一個簡單的Demo,是登錄功能的
首先是模型的,先定義了一個IUser接口,屆時展示器想調用模型的方法是就通過這個接口來調用,免除了對模型其他成員的訪問
1 public interface IUser 2 { 3 bool CheckLogin(string user, string password); 4 }
再由一個IModelUser實現這個接口
1 public class UserModel:IUser 2 { 3 public bool CheckLogin(string user, string password) 4 { 5 if (user == "admin" && password == "123456") 6 return true; 7 return false; 8 } 9 }
接着到展示器
1 public class LoginPresenter:PresenterBase<ILoginView> 2 { 3 [Inject] 4 public IUser UserModel { set; get; } 5 6 public LoginPresenter(ILoginView view):base(view) 7 { 8 this.View = view; 9 this.View.OnLogin += new MyEventHandler(View_OnLogin); 10 } 11 12 void View_OnLogin(object sender, MyEventArgs e) 13 { 14 bool result = UserModel.CheckLogin(this.View.IDBoxText, this.View.PasswordBoxText); 15 e.OptionResult=result; 16 if(result) 17 { 18 PresenterManager.ShowView("SystemPresenter"); 19 (sender as Form).Hide(); 20 } 21 } 22 }
在構造展示器實例時,給視圖的事件綁定一個方法,相應登錄視圖的登錄驗證請求,在改方法內調用模型的方法驗證用戶名密碼,把結果通過委托的參數傳遞給視圖。如果驗證通過了就隱藏登錄視圖,顯示主界面。
最后到視圖
1 public interface ILoginView:IView 2 { 3 event MyEventHandler OnLogin; 4 string IDBoxText { get; set; } 5 6 string PasswordBoxText { get; set; } 7 } 8 9 public partial class LoginView : Form,ILoginView 10 { 11 public LoginView() 12 { 13 InitializeComponent(); 14 } 15 16 private void button1_Click(object sender, EventArgs e) 17 { 18 MyEventArgs args=new MyEventArgs(); 19 if (OnLogin != null) OnLogin(this, args); 20 if (!args.OptionResult) 21 MessageBox.Show("Fail"); 22 } 23 24 public event MyEventHandler OnLogin; 25 26 public string PasswordBoxText 27 { 28 get { return this.tbPw.Text; } 29 set { this.tbPw.Text = value; } 30 } 31 32 public string IDBoxText 33 { 34 get { return this.tbID.Text; } 35 set { tbID.Text = value; } 36 } 37 }
視圖這里ILoginView是繼承了IView接口,里面聲明了登錄視圖應該外放的事件和屬性,那登錄界面來說
雖然很明顯看得出ID后的輸入框的值是用戶ID,Password后面的輸入框的值是用戶密碼,但是這些對於一個視圖來說都是不知其含義的,知道含義的是展示器,視圖只是把值外放出去給展示器獲取。正如一位園友說的,視圖就該盡量吧控件多外放出去。不過我覺得某些簡單的界面邏輯還是放在視圖上比較好,例如單擊了某個按鈕使得另一個輸入框變灰之類的。
這樣就牽強地使用了一下MVP模式,有位園友在討論MVC時說過,沒發揮到MVC的優勢時干脆用回以前的WebForm,MVP也一樣吧,期待能真正用上它的時候。由於最近都是從事C/S的開發,對C/S比較熟悉,做的這個小嘗試也是用WinForm的,但轉到WebForm上估計也不難,展示器管理那里要更改一下。