白话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模式了。

 

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

2
0
(请您对文章做出评价)
« 上一篇:Prism研究 目录
» 下一篇:从Event折腾到Command
posted @ 2009-10-09 08:16 包建强 阅读(3523) 评论(20)  编辑 收藏 网摘 所属分类: Prism

  回复  引用  查看    
#1楼2009-10-09 09:03 | 温景良(Jason)      
难道是沙发.喜欢mvp模式
  回复  引用    
#2楼2009-10-09 09:05 | Rui Costa[未注册用户]
沙发,正在研究Prism,这篇文章很有用,谢过先
  回复  引用  查看    
#3楼2009-10-09 09:20 | asheng      
大哥,我就搞不懂了 为什么每篇文章 都是“置顶”呢?
  回复  引用  查看    
#4楼2009-10-09 09:57 | 亚历山大同志      
所谓MVP模式就是微软的MVP捣鼓出来的模式 O(∩_∩)O哈哈~
  回复  引用  查看    
#5楼2009-10-09 10:05 | 上不了岸的鱼      
关注MVP模式
  回复  引用  查看    
#6楼2009-10-09 10:59 | 阿K&LiveCai      

引用亚历山大同志:所谓MVP模式就是微软的MVP捣鼓出来的模式 O(∩_∩)O哈哈~



太有才。呵呵

  回复  引用  查看    
#7楼2009-10-09 11:49 | 韦恩卑鄙      
持续关注中
  回复  引用  查看    
#8楼2009-10-09 14:26 | Prime Li      
如果是WPF/SL项目建议使用MVVM模式,只需要2个类:View和ViewModel。如果再进一步,使用附加属性,附加行为来扩展WPF本身的事件机制的话(桥接事件到方法),那么View.cs可以精简到只有一个含InitializeComponent()的构造方法。如此一来,XAML和逻辑完全就分离了。UI设计师可以设计完XAML,然后开发者只需在XAML上写绑定,附加属性,附加行为,写ViewModel,系统就跑起来了。
  回复  引用  查看    
#9楼[楼主]2009-10-09 14:37 | 包建强      
@Prime Li
如果再进一步,是使用实体类,还是使用附加属性?

  回复  引用  查看    
#10楼2009-10-09 15:09 | 风云      
讲的不错,很深入,我也将写一篇利用消息总线来简化MVP中V的事件处理
  回复  引用  查看    
#11楼2009-10-09 16:00 | ξ箫音ξ      
支持小包兄弟一下,写的挺细。其实MVP是MVC的引申,由于代码与界面分离的性质,非常适合大项目团队协作开发,一般小项目代码量较大。
  回复  引用    
#12楼2009-10-09 18:21 | waiting zhan[未注册用户]
老赵怎么还不来............

  回复  引用  查看    
#13楼2009-10-10 09:57 | Sonven      
收藏了,还没学习过mvp模式呢。
  回复  引用  查看    
#14楼2009-10-10 10:00 | kumaws      
太及时了!!我正需要系统学习而无从下手。tks!
  回复  引用    
#15楼2009-10-10 13:43 | 我看错了[未注册用户]
看标题还以为是MS的MVP呢.
哈哈

  回复  引用  查看    
#16楼2009-10-10 20:27 | hongxuan20      
因为曾经使用过WPF和Prism,包括MVVM,对这类文章比较感兴趣。看了此篇,有些话不吐不快。

1,很高兴老包得出的结论:“有人画蛇添足,在MVVM中又添加了一个Presenter,真的是没有必要。”不管此结论正确与否,至少我也很认同。还跟老包小小讨论过。以后我可以自豪地对别人说:老包也是这么认为的!:)

2, 最简单的View其实比老包见过的更简单,就是
public partial class PanelView : Window
{
public PanelView()
{
InitializeComponent();
}
}
那么View怎么把自己的DataContext设置成对应的ViewModel呢?写个DataTemplate即可:
<DataTemplate DataType="{x:Type ViewModels: PanelViewModel}">
<PanelView />
</DataTemplate>
然后代码里window.Content = PanelViewModel; //而不是PanelView。
就可以了。这是“隐式”设置View的DataContext,老包代码是“显式”设置。虽然都差不多,但我是喜欢隐式的方法,感觉酷一点.

3, MVVM里如果还有IView,而把它称为“最高境界”的MVP,我是不敢苟同的。事实上我认为,MVVM里不需要IView。一切都是VM和View的绑定,实在不清楚留个IView给VM来操作有什么必要,也许我做过的项目不够大的关系?
IView是MVP里常见的接口,无非是为了让Presenter依赖IView,View从IView继承,并且依赖Presenter。典型的解耦,背后的思想就是把View抽象出来,IView就是View的抽象。而在MVVM里呢?View的抽象是什么?我认为就是VM。看看他们之间的双向绑定,VM活脱脱就是View的化身。既然View已经有了个抽象叫VM,何必又把它抽象成IView?我还是认为有IView是MVP的影响太大的关系。没有摆脱MVP的逻辑。

4, 关于View的生命周期,Sacha Barber的Cinch框架似乎是在ViewModelBase上实现了完整的生命周期(对Cinch大致看了一下,没有仔细研究)。他的基于Cinch的Demo例子里,所有的View都只有简单的InitializeComponent()一句代码。不过老包说的需要动态创建控件的确是个理由。但即使撇开Prism不说的话,动态创建控件其实是很容易的,不需在View上作文章,只需把这个需要创建的控件做成一个UserControl(一个小View),小View当然有个对应的小ViewModel(一切都MVVM),然后那个大的ViewModel动态把小ViewModel加进来就行了,还是那个思想:ViewModel就是View的化身。只不过有没有必要搞得这么“纯粹”,那就见仁见智了。

5,一个WPF项目基于Prism是不错的,但我觉得是不够的,一个框架有它的发挥领域,但每个项目有自己的实际情况。以我以前的经验,因Prism里确实有不少很好的实践,我们不可能不关注,但我们当时是把它有价值的代码直接Copy下来,放进我们自己的框架里,包括EventAggregator, DelegateCommand...,另外它的bootstrapper是个很好的思路,我们是引用过来并且稍加改良,个人觉得改良后效果更好。另外,如果是现在的我,还会把Cinch里的一些思想也加进去,再把DDD的一些思想加进去,来丰富整个框架。另外,个人觉得,Prism里的Module在中小型项目里其实意义不大,而这是Prism最重头的地方,这也是我们当时没有直接用Prism,而是从中提取有价值代码出来的原因。

以上是个人一些想法,自己又懒得写博客,借贵地抒发一下,不当之处请老包批评指正。

  回复  引用  查看    
#17楼[楼主]2009-10-10 22:52 | 包建强      
关于你说的第3点,IView的使用,是为了Model中的数据变化,回调View中的方法,从而改变控件的属性。
除非你的Command只执行Model逻辑,而不影响控件,不然,还是要在界面上有所反应的。

  回复  引用  查看    
#18楼[楼主]2009-10-10 22:56 | 包建强      
你说的第5点,我同意。我在这个系列的Demo里,都基本不依赖于Prism,或者只使用其中的1-2个类。
Prism的Command、Event、AttachedBehavior、MVP、Unity都可以单独拿出来。
Prism里面的主要代码是Region,这才是它独有的,不过,可惜的是,大家都只使用了其中的皮毛,而且普遍上,大家的眼球都被Command、Event、AttachedBehavior、MVP、Unity吸引走了,对于Region,则是够用就好。
这其实就舍本求末了。

  回复  引用  查看    
#19楼2009-10-26 15:24 | 青林一霸      
请问博主:在这种模式下,数据持久化如何实现呢?
  回复  引用  查看    
#20楼[楼主]2009-10-26 16:50 | 包建强      
在ViewModel中,不是存有这些数据么?它们以属性的方式存在。对于外部的数据,可以做成静态类。
Silverlight/WPF下还有存储隔离区技术。