白话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 string Name { get; set; }
}
2)View
在View中创建Model属性,在set方法中,根据传进来的Model实体,将其属性分配给View中各个控件的属性;而在get方法中,则根据当前各个控件的属性,初始化出来一个Model实体:
{
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 PanelPresenter(PanelView view)
{
this.View = view;
//初始化Model
this.View.Model = new PanelPresenterationModel() { Name = "Bao, Jianqiang" };
}
public PanelView View { get; set; }
}
4)修改Main函数,由原先直接加载View:
static void Main()
{
//省略一些语句
Application.Run(new PanelView());
}
改为先创建Presenter实例,然后加载Presenter的View属性:
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 void btnModify_Click(object sender, EventArgs e)
{
if (ButtonClick != null)
{
ButtonClick(sender, e);
}
}
相应的,在Presenter的构造函数中为该事件挂上匿名方法:
{
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 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 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 IPanelView View { get; set; }
}
3. 在ViewModel中,可以有一些属性,直接与View中的元素进行绑定;也可以把这些属性抽象为一个Model实体,一起绑定到View上;二者可以兼而有之。关于这方面的讨论,请参见《Prism研究之 巧妙使用INotifyPropertyChanged》
{
public PanelViewModel()
{
this.Panel = new PanelInfo() { Name = "Bao, Jianqiang" };
}
public PanelInfo Panel { get; set; }
}
4. 在ViewModel中定义Command及其OnExecute方法。
{
public PanelViewModel()
{
this.ClickCommand = new DelegateCommand<object>(OnClick, arg => true);
}
public void OnClick(object obj)
{
this.Panel.Name = "Jax.Bao";
}
public DelegateCommand<object> ClickCommand { get; set; }
}
5. 由ViewModel管理数据间的交互,比如说下面即将介绍的那个ModelPropertyChanged方法。
MVVM和MVP本质上是一致的,大家在熟悉了MVP之后,自然很容易理解MVVM。所以,我在上面只是给出了MVVM独有的几个特征。更多MVVM的讨论,请大家耐着性子继续读下去。
(三)万变不离其中,玩的就是心跳
1)View中的代码到底精简到什么程度?
这是我见过的最简单的View:
{
public PanelView()
{
InitializeComponent();
}
public PanelViewModel Model
{
get
{
return this.DataContext as PanelViewModel;
}
set
{
this.DataContext = value;
}
}
}
对于View-first和Presenter-first的不同,以及MVP和MVVM的不同,一共有四种编程方式,详细的讨论请参见《Prism研究 之 View-first和Presenter-first》
总结完以上4种情况,我曾经一度认为View中除了构造函数和Model(MVVM下是ViewModel)属性,就不该有其它的成员了。的确,View中的控件事件都被转换为Command放在Presenter或ViewModel中,View中的属性都被转为了Model的一部分。按说,没有剩下什么了。
但是,随着代码越写越多,我发现这只是一种乌托邦的想法罢了。
1.首先,是Form的Loaded事件,这是我绞尽脑汁也不能转换为Command的,如下所示:
{
this.regionManager.Regions[RegionNames.AlertRegion].Add(this.container.Resolve<AlertView>());
this.regionManager.Regions[RegionNames.FlightPanelRegion].Add(this.container.Resolve<FlightPanelView>());
}
像Loaded这样的生命周期事件还有很多,比如说Unloaded、Initialized,我们无法从数据的角度来判断什么时候执行这些事件。没办法,WPF从内而外仍然是基于Event机制的,所以我们不可能彻底把所有的Event都转换为Command。
而且,在做项目的时候,要学会放弃,所以,即使用AttachedBehavor花了2周时间把Loaded事件实现了,也是没有意义的。我是一个从来不服输的程序员,就算搭上2周业余时间也要实现Loaded事件——成就感胜过一切,但是这次,包包真的是没有脾气了。
可是,后来我想明白了,就是把它转换为Command而从View中移除,又有什么意义呢?这些生命周期事件与我的数据(Model)是没有任何关系的。于是我又释然了。MVP模式只是要求把和数据(Model)有关的事件抽象为Command,这一点和我把Loaded事件留在View中并不冲突。
有时候,残缺也是一种美。告诉世人,没有十全十美的事物,包括编程。
2.其次,如果执行Presenter的Command时,需要反向操作View怎么办?
举一个最简单的例子,点击Button,动态添加一个新的TextBox在当前的View中。
{
StackPanel sp = new StackPanel { HorizontalAlignment = HorizontalAlignment.Left, Orientation = Orientation.Horizontal };
sp.Children.Add(new TextBox { Width = 45, Margin = new Thickness(40, 4, 4, 4) });
sp.Children.Add(new TextBox { Width = 45, Margin = new Thickness(4, 4, 4, 4) });
StationPanel.Children.Add(sp);
}
这个OnAddStationCommandExecute方法操作的是View中的控件而不是数据,所以要放在View中,然后在ViewModel的Command中调用这个方法(这里使用MVVM模式来实现):
public void OnAddStationCommandExecute()
{
View.AddStation();
}
这里使用到了控制反转(IoC)的思想,如下图所示:
在当前场景中,IoC的UML图是这样的:
扯远了,把话题拉回来,这个OnAddStationCommandExecute方法使得原先简洁的View变得复杂了,如果这样的方法多了,View最终还是会有上千行代码量的。但是大家发现了没有,像OnAddStationCommandExecute这样的方法都是来自于View的接口,只会在ViewModel中被调用;其次这些方法只和界面(UI)有关,而与数据(Model)无关。可以说,这样的设计已经实现了UI和Model之间的解耦。
总结以上两种情况,View中的代码到底能精简到什么程度,是取决于业务逻辑的。对于业务逻辑简单的View,仅包含构造函数和Model属性是可行的;而对于业务逻辑复杂的View,我们把ViewModel(或Presenter)中对View的操作方法统一放在View的接口中,就像上面那个例子一样。
我估计,由于技术水平参差不齐,很多程序员对MVP和Command理解不一,他们会以此为借口,而把那些转换成Command比较麻烦的事件遗留在View中,比如说TextBlock的Click事件。对此,我给出判断一个方法是否应该出现在View中的准则:看这个方法是否和数据(Model)有关。
2)MVP和ViewModel的比较
MVP:View的Context是和Model绑定的,绑定的动作是由Presenter完成的。Presenter的作用:
1. 在构造函数中初始化Model。
2. 管理着Model中属性之间的联系。比如属性A的变化导致属性B的变化。
3. 包括Command,以及相应的Execute方法。
MVVM:View的Context是和ViewModel绑定的,ViewModel中包括Model,也可以直接包括一些要绑定的属性,这取决于设计。ViewModel包括Command,以及相应的Execute方法。ViewModel中还管理着Model中属性之间的联系。比如属性A的变化导致属性B的变化。
由此看来,MVP和MVVM只是“把界面从逻辑中剥离”的两种实现方式,达到的效果相同,没有优劣之分。初学者可能会为其外表所迷惑,但是,写了几个项目之后,就能感受到异曲同工之妙。
3)真的需要Controller吗?
总是看到有人画蛇添足,在MVVM中又添加了一个Presenter,真的是没有必要。既有MVVM已经能帮我们处理所有的企业级逻辑。我想,他们这样做有可能是从MVP迁移到MVVM,习惯了原有模式——在Presenter里面处理逻辑。
此外,Prism提供的Demo都是基于MVP的,它提出了一个新观点,就是在MVP的基础之上,再抽象出来一个Controller层,其实,这个层是可以合并到Presenter中的。如果换作是我,我是不会再搞出一个Controller的,因为它会使原先的MVP变得复杂。这就像我们习惯三层架构,现在升级为N层,无非是“社会分工”更细了。
4)既然MVP这么好用,为什么不在Visual Studio中集成呢?
这个问题我想了很久。
有朋友和我说,他写了一个MVP的项目模板。其实,真的没有必要。
因为MVP是很灵活的一个东西。
比如说,根据View-first,还是Presenter-first,可以有两种形式的写法。
又比如说,View和Presenter中是否需要依赖注入?如果有,是构造函数注入,还是属性注入?
对于MVP的孪生兄弟MVVM,也有同样的问题。
此外,在ViewModel中,一个需要绑定的属性究竟是放在ViewModel中,还是放在Model中,要根据具体情况来定。
总之,太多太多的因素影响着我们。如果根据MVP模板来创建项目,我们就无法灵活创建有自己特色的MVP模式了。
设计,是一种美。就像盖大楼,如果每座房屋都是千篇一律,那么也就不存在架构师了。