白话MVP

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

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

 

(一)MVP之前世今生

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

      clip_image002

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

      1)Model

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

        public class PanelPresenterationModel
        {
            
public string Name { getset; }
        }

      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 { getset; }
    }

      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模型就建立起来了,效果图如下所示:

      clip_image004

      代码下载:WindowsFormsApplication2.zip

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

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

      clip_image006

      为此,我们要在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如下所示:

      clip_image008

 

      (二)MVVM之横空出世

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

      clip_image010

      我们将上面以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 { getset; }
    }

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

    public class PanelViewModel
    {
        
public PanelViewModel()
        {
            
this.Panel = new PanelInfo() { Name = "Bao, Jianqiang" };
        }

        
public PanelInfo Panel { getset; }
    }

      4. 在ViewModel中定义Command及其OnExecute方法。

    public class PanelViewModel
    {
        
public PanelViewModel()
        {
            
this.ClickCommand = new DelegateCommand<object>(OnClick, arg => true);
        }

        
public void OnClick(object obj)
        {
            
this.Panel.Name = "Jax.Bao";
        }

        
public DelegateCommand<object> ClickCommand { getset; }
    }

      5. 由ViewModel管理数据间的交互,比如说下面即将介绍的那个ModelPropertyChanged方法。

     

      MVVM和MVP本质上是一致的,大家在熟悉了MVP之后,自然很容易理解MVVM。所以,我在上面只是给出了MVVM独有的几个特征。更多MVVM的讨论,请大家耐着性子继续读下去。

 

      (三)万变不离其中,玩的就是心跳

      1)View中的代码到底精简到什么程度?

      这是我见过的最简单的View:

    public partial class PanelView : Window
    {
        
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的,如下所示:

        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            
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中。 

        public void AddStation()
        {
            StackPanel sp 
= new StackPanel { HorizontalAlignment = HorizontalAlignment.Left, Orientation = Orientation.Horizontal };
            sp.Children.Add(
new TextBox { Width = 45, Margin = new Thickness(40444) });
            sp.Children.Add(
new TextBox { Width = 45, Margin = new Thickness(4444) });
            StationPanel.Children.Add(sp);
        }

      这个OnAddStationCommandExecute方法操作的是View中的控件而不是数据,所以要放在View中,然后在ViewModel的Command中调用这个方法(这里使用MVVM模式来实现):

        AddStationCommand = new DelegateCommand<object>(OnAddStationCommandExecute);
        
        
public void OnAddStationCommandExecute()
        {
            View.AddStation();
        }

      这里使用到了控制反转(IoC)的思想,如下图所示:

clip_image012

      在当前场景中,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模式了。

 

      设计,是一种美。就像盖大楼,如果每座房屋都是千篇一律,那么也就不存在架构师了。

posted @ 2009-10-09 08:16  包建强  Views(16364)  Comments(22Edit  收藏  举报