白话MVP

前言一:没有想到的是,这篇文章竟然断断续续写了一个多月,期间反复改了多次,思想也经历了好几次升华。本来文章的题目是《MVP之七十二变》,但是最终发现变来变去其实就只有两个模式,MVP和MVVM,而后者还是从前者中衍生的,二者形差而神似,正所谓——条条大路通罗马。

前言二:本文,以及后面的几篇文章《从event折腾到command》、《AttachedBehavior技术详解》、《包式波动理念》共同构成了Prism开发的四部曲(这么名字有点别扭哦)。这一系列文章,都是基于这两个多月来对公司的Silverlight项目进行重构时的经验之谈。

(一)MVP之前世今生

MVP模式,顾名思义即Model—View—Presenter。一言以蔽之,就是用Presenter控制界面(View)和数据(Model)的交互关系。通用MVP的UML图如下所示(也适用于Winform和ASP.NET,后者又将Presenter称为Controller):

接下来,我要给出Winform下的MVP编程模型的模板,任何地方都可以套用,大致分为以下几步:

1)Model

Model是一个只包含属性的实体类,这些属性分别与View中需要绑定的控件属性相对应。比如说,当前这个例子的Model,只有一个属性Name,绑定到View中Label的Text属性。

public class PanelPresenterationModel
{
public string Name { get; set; }
}

2)View

在View中创建Model属性,在set方法中,根据传进来的Model实体,将其属性分配给View中各个控件的属性;而在get方法中,则根据当前各个控件的属性,初始化出来一个Model实体:

public partial class PanelView : Form
{
public PanelView()
{
InitializeComponent();
}

private PanelPresenterationModel model;

public PanelPresenterationModel Model
{
get
{
return model as PanelPresenterationModel;
}

set
{
model = value as PanelPresenterationModel;
label1.Text = model.Name;
}
}
}

3)Presenter

在Presenter中,创建View属性。在构造函数中建立Model、View和Presenter的关系,并初始化Model(也就是)的值:

public class PanelPresenter
{
public PanelPresenter(PanelView view)
{
this.View = view;

//初始化Model
this.View.Model = new PanelPresenterationModel() { Name = "Bao, Jianqiang" };
}

public PanelView View { get; set; }
}

4)修改Main函数,由原先直接加载View:

[STAThread]
static void Main()
{
//省略一些语句
Application.Run(new PanelView());
}

改为先创建Presenter实例,然后加载Presenter的View属性:

[STAThread]
static void Main()
{
//省略一些语句
PanelPresenter presenter = new PanelPresenter(new PanelView());

Application.Run(presenter.View);
}

至此,一个MVP模型就建立起来了,效果图如下所示:

代码下载:WindowsFormsApplication2.zip

补充一点,对于上述的Main函数,采用的是先实例化Presenter,后实例化View的方式,我们称之为Presenter-first。其实,还有另一种写法,就是先实例化View,后实例化Presenter,也就是View-first。这两种方式没有优劣之分。我们将上述示例修改为View-first的方式,仅供参考。
代码下载:WindowsFormsApplication1.zip

以上代码并没有展示MVP的全部优点,于是,我们为这个程序添加一个按钮,点击后修改窗体上显示的文本。这样把Model从View中剥离出来的好处就看到了。

为此,我们要在View中添加一个ButtonClick事件,点击按钮就会触发这个事件:

public EventHandler ButtonClick;

public void btnModify_Click(object sender, EventArgs e)
{
if (ButtonClick != null)
{
ButtonClick(sender, e);
}
}

相应的,在Presenter的构造函数中为该事件挂上匿名方法:

this.View.ButtonClick += delegate
{
this.View.Model = new PanelPresenterationModel() { Name = "Jax.Bao" };
};

——这样,点击View中按钮的时候,就会由Presenter来修改Model中的Name属性。

以上这个例子只是为了说明Winform也支持MVP模式。

代码下载:MVPDemo1.zip

但是我们看到,在传统的Winform中,View中的代码还是很多,究其原因,是缺少一种机制,使得View中的控件属性和Model中的属性绑定在一起,其中一个的变化会导致另一个也跟着变化。于是,WPF和Silverlight应运而生,它们的出现,掩盖了MVP中数据绑定的复杂度,将View中的代码简化到极致。

我们知道,在WPF和Silverlight的任何一个级别的UserControl中,都拥有一个DataContext属性,于是我们可以把Model绑定到这个属性上,而不用在View的内部声明一个字段来保存Model属性。WPF版本的代码示例如下:

代码下载:MVPDemo2.zip

代码是不是简单了不少?起码当数据改变时,我们不用再关心随之带来的控件上的变化。你也许会说,那个按钮的click事件放在那里看上去很碍眼,额,这个就不是WPF本身能解决的问题啦,于是Prism应运而生。

Prism版本的Demo提供如下:MVPDemo3.zip

我们看到,在Prism中,按钮的click事件被抽象为Command命令而写在xaml中:

同时我们在Presenter中为其附加上该命令所要执行的方法OnClick。

终于,View中的代码只剩下了以下几行:

public partial class PanelView : Window
{
public PanelView()
{
InitializeComponent();
}

public PanelPresenterationModel Model
{
get
{
return this.DataContext as PanelPresenterationModel;
}

set
{
this.DataContext = value;
}
}
}

看到没?这就是MVP模式的最高境界——View中的代码仅包括Model属性。所有使用MVP模式编程的开发者都要尽可能把Event转换为Command实现。当然,有一些特殊事件是不能转换为Command的,我会在下一篇文章《从event折腾到command》中进行介绍。

总结一下,在WPF中,建立了绑定机制的的MVP架构更加丰满,UML如下所示:

(二)MVVM之横空出世

MVVM模式,就是Model—View—ViewModel的简称,是从MVP模式中衍生出来的。UML如下所示:

我们将上面以MVP模式编写的WPF代码修改为MVVM模式,代码如下:

代码下载:MVPDemo4.zip

根据上面的代码,我们发现,MVVM模式在形式上具有几个特点:

1. View不再与Model直接绑定,而是绑定ViewModel

public partial class PanelView : Window, IPanelView
{
public PanelView()
{
InitializeComponent();
}

public PanelView(PanelViewModel viewModel)
: this()
{
this.Model = viewModel;
this.Model.View = this;
}

public PanelViewModel Model
{
get
{
return this.DataContext as PanelViewModel;
}
set
{
this.DataContext = value;
}
}
}

2. 在ViewModel中,保存着对IView的引用,这个IView,通常是View所实现的接口。

public class PanelViewModel
{
public IPanelView View { get; set; }
}

3. 在ViewModel中,可以有一些属性,直接与View中的元素进行绑定;也可以把这些属性抽象为一个Model实体,一起绑定到View上;二者可以兼而有之。关于这方面的讨论,请参见《Prism研究之 巧妙使用INotifyPropertyChanged》

public class PanelViewModel
{