没有记错的话,应该是去年此时,我和另一个同事在技术经理的指导下对我们开发人员使用的开发辅助工具进行重构。当时技术经理提出运用MVP模式进行开发,因为另一同事在上一家公司运用过此种开发模式,便让他写了一个Demo,我也是照着葫芦画瓢,赶鸭子上架的开干起来了,当时也在网上了了解了这个开发模式,并没有深入的去研究它,现在想想确实不该啊,为什么只让自己做一个搬砖的人了,虽然身处搬砖的位置。今天我就来说一说对MVP开发模式的理解。
MVP英文全称:Model-View-Presenter,翻译过来就是模型-视图-表示器,它是MVC模式的一个变种,表面上看两种模式的M和V的职责应该是一样的,MVC的Controll与MVP的Presenter都是用来处理业务逻辑的。但是它们之间又有不一样的地方,就是在MVP中,View只与Presenter交互,Model只与Presenter交互,View与Model不直接交互,MVC中就不一样了,View中是可以直接引用Model的,也就是说MVP中将View与Model进行了隔离。比喻说我们在用asp.net web forms 或者 windows forms开发项目时,我们就可以运用MVP设计模式,它能避免View与Model之间的偶合,并且可以降低Presenter对View的依赖,最直接的例子就是web forms与windows forms并存的项目。
其实到这里,我们最需要关心的就是View到底怎样与Presenter进行交互。
在之前那个项目中,View与Presenter之间的交互方式可以用下面的类图简单表示出来。
这个类图当中有一点没有体现出来,就是在Presenter中都会对View中控件的事件进行注册。其潜台词就是运用了事件机制,既然是事件机制,那么对于数据的流向自然是单向的。View里面仅仅实现单纯独立的UI处理逻辑,处理的数据都是Presenter推送过来的。所以View里面尽可能的不维护数据状态,Presenter所需的关于View的状态应该在接收到View发送的用户交互请求的时候一次得到。
在下面的讲述中,针对上面的类图,多出来了一个IView接口,看看类图。
被动视图模式Passive View (PV)。在这种模式下,View只负责显示,Presenter负责响应用户动作,它的特点就是View完全是被动的,被Presenter操控,所以所有的视图逻辑全部在Presenter里。PV主要的目的是为了测试,View不干什么事情,Presenter甚至可以脱离UI环境而进行测试。下面我们用一个例子来说明PV模式。如下图:
一个简单的查询界面。用户在界面上输入姓名,选择年级,那么符合条件的数据就会在DataGridView里面显示出来。在这里有一点区别于之前的项目,就是每一个VIew都有一个IView与之对应。我们先看下IStudentView的代码。
public interface IStudentMgrView { TextBox TxtName { get; } ComboBox CmbGrade { get; } Button BtnQuerys { get; } DataGridView DgvData { get; } }
没有任何方法,只有4个属性,分别与界面上的控件对应。在看看View的代码。
public partial class StudentMgr : Form,IStudentMgrView { private StudentMgrPresenter presenter; public StudentMgr() { InitializeComponent(); presenter = new StudentMgrPresenter(this); } public TextBox TxtName { get { return this.txtName; } } public ComboBox CmbGrade { get { return this.cmbGrade; } } public Button BtnQuerys { get { return this.BtnQuery; } } public DataGridView DgvData { get { return this.dgvData; } } }
可以看出View里面的代码是相当简单,只有Get,再就是构造函数中对Presenter进行初始化。接下来继续看Presenter。
public class StudentMgrPresenter { private IStudentMgrView view { get; set; } private StudentRepository repository { get; set; } public StudentMgrPresenter(IStudentMgrView view) { this.view = view; this.repository = new StudentRepository(); //1. 初始化界面 InitView(); //2. 注册事件 RegisterEvent(); } private void InitView() { BindGrade(); } private void BindGrade() { this.view.CmbGrade.Items.Add(""); this.view.CmbGrade.Items.Add("七年级"); this.view.CmbGrade.Items.Add("八年级"); this.view.CmbGrade.Items.Add("九年级"); this.view.CmbGrade.SelectedIndex = 0; } private void RegisterEvent() { this.view.BtnQuerys.Click += new EventHandler(BtnQuerys_Click); } private void BtnQuerys_Click(object sender, EventArgs e) { var name = this.view.TxtName.Text; var grade = this.view.CmbGrade.SelectedItem.ToString(); var students = repository.QueruStudents(name, grade); FillDataGrid(students); } private void FillDataGrid(IList<Student> students) { this.view.DgvData.DataSource = students; } }
Presenter里面,有对界面初始化的部分,对按钮进行事件注册,对DataGridView进行数据绑定。从以上看出,所有的界面逻辑都在Presenter里面体现出来了,并且View与Model完全独立开来。
监听控制器模式Supervising Controller(SC)。在这种模式下,VIew通过数据绑定和业务模型进行关联,Presenter关注简单的界面绑定逻辑。接下来看看SC模式下View与IView的代码。
public interface IStudentMgrView { void BindGrade(IList<string> grades); void LoadStudentData(IList<Student> students); event EventHandler<QueryEventArgs> OnStudentLoaded; } public class QueryEventArgs : EventArgs { public string name { get; private set; } public string grade { get; private set; } public QueryEventArgs(string name,string grade) { this.name = name; this.grade = grade; } }
在IView里面定义了两个绑定方法和一个事件。分别是绑定年级下拉框以及数据展示的DataGridView,事件是在按钮查询时用到。接下来看看View的代码。
public partial class Main : Form,IStudentMgrView { private StudentMgrPresenter presenter; public Main() { InitializeComponent(); presenter = new StudentMgrPresenter(this); } private void Main_Load(object sender, EventArgs e) { presenter.InitView(); } public void BindGrade(IList<string> grades) { grades.ToList().ForEach(grade => { this.cmbGrade.Items.Add(grade); }); } public void LoadStudentData(IList<Student> students) { this.dgvData.DataSource = students; } private void BtnQuery_Click(object sender, EventArgs e) { var name = this.txtName.Text; var grade = this.cmbGrade.SelectedItem == null ? "" : this.cmbGrade.SelectedItem.ToString(); QueryEventArgs eventArgs = new QueryEventArgs(name, grade); if (null != OnStudentLoaded) { OnStudentLoaded(this, eventArgs); } } public event EventHandler<QueryEventArgs> OnStudentLoaded; }
在上述代码中值得一说的就是在界面的加载事件里面,会调用Presenter里面的InitView方法。InitView里面也就是做两件事情,一是初始化下拉框数据,另外就是注册IView里面定义的OnStudentLoaded事件。看代码。
public class StudentMgrPresenter { private IStudentMgrView View { get; set; } private StudentRepository Repository { get; set; } public StudentMgrPresenter(IStudentMgrView View) { this.View = View; this.Repository = new StudentRepository(); } public void InitView() { var grades = new List<string>(); grades.Add("七年级"); grades.Add("八年级"); grades.Add("九年级"); this.View.BindGrade(grades); this.View.OnStudentLoaded += new EventHandler<QueryEventArgs>(View_StudentLoaded); } private void View_StudentLoaded(object sender, QueryEventArgs e) { var students = Repository.QueruStudents(e.name, e.grade); this.View.LoadStudentData(students); } }
从上述PV与SC的示例代码当中,我们可以总结出他们相同的地方,View不会去关注Presenter,但是Presenter会去关注View。然后PV中,View与Model完全隔离开,但是在SC当中View与Model通过数据绑定进行关联的。在PV当中,要求将界面可操作的控件全部定义在IView里面,做过企业信息化系统的人都知道,有的界面不是能用复杂形容的,如果这个时候用PV,在写接口的时候是不是有种崩溃的感觉。因为接口膨胀,那么在Presenter里面,原本简单的控件控制逻辑将会变得复杂,这个时候SC就是很好的选择,如果非要用MVP模式。在SC当中,IView不必去关心界面上的每一个控件UI逻辑,这些可以被View分担,IView里面只会定义一些处理逻辑的方法或者其他。不管怎样,Presenter在两种变体中依旧占主要位置,好比话事人,龙头老大。