浅析WPF中MVVM模式下命令与委托的关系

  各位朋友你们好,我是Payne,欢迎你们关注个人博客,个人博客地址是http://qinyuanpei.com。最近由于项目上的缘由开始接触WPF,或许这样一个在如今来说显得过期的东西,我猜你们不会有兴趣去了解,但是你不会明白对某些保守的项目来说,安全性比先进性更为重要,因此当你发现银行这类机构还在使用各类“复古”的软件系统的时候,你应该相信这类东西的确有它们存在的意义。与此同时,你会更加深入地明白一个道理:技术是否先进性和其流行程度自己并没有直接联系。由此咱们能够推论出:一项不流行的技术不必定是由于它自己技术不先进,或许仅仅是由于它没法知足商业化的需求而已。我这里的确是在说WPF,MVVM思想最先由WPF提出,然而其发扬光大倒是由于前端领域近年来比较热的AngularJS和Vue.js,咱们这里表达的一个观点是:不少你觉得很是新潮的概念,或许仅仅是被人们从新赋予了新的名字,当你理清这一切的前因后果之后,你会发现这一切并无什么不一样。这符合我一向的主张:去发现问题的实质、不要被框架束缚、经过共性来消除差别,因此在今天这篇文章里,我想说说WPF中MVVM模式下命令与委托的关系。前端

什么是MVVM?

  既然说起MVVM,那么咱们就无可避免的须要知道什么是MVVM。咱们在本文开篇已经提到,MVVM这个概念最先由微软提出,具体来说是由微软架构师John Gossman提出的。我我的更喜欢经过将MVC、MVP和MVVM这三者横向对比的方式来增强理解,由于这从某种意义上来说,这是一个逐步改进和演化的过程。咱们经常谈及软件的三层架构,咱们经常对MVC耳濡目染以至将其神化,可事实上它们是某种在思想上无限接近的理念而已。程序员

MVC模式示意图

  首先,咱们从最简单的MVC开始提及,做为最经常使用的软件架构之一,咱们能够从上面的图示中看到,MVC实际上是很是简单的一个概念,它由模型(Model)、视图(View)和控制器(Controller)三部分组成,创建在一个单向流动的通讯基础上,即View通知Controller响应用户请求,Controller在接到View的通知后会更新Model内的数据,而后Model会将新的数据反馈给View。咱们发现这个设计可使软件工程中的关注点分离,咱们注意到经过MVC模式,咱们实现了视图和模型的分离,经过控制器这个胶水层让二者间接联系起来,因此MVC的优势是让各个模块更好的协做。那么,它的缺点是什么呢?显然,视图和控制器是高度耦合的,由于控制器中无可避免地要访问视图内的元素,因此控制器注定没法在这尘世间独善其身。要知道最先的MVC架构是基于观察者模式实现的,即当Model发生变化时会同时通知View和Controller,因此咱们很快就能够认识到:咱们从古至今的全部努力,都是为了让视图和模型彼此分离,咱们在这条路上越走越远,幸运的是一直都不忘初心。web

MVP模式示意图

  接下来,咱们为了完全地让视图和模型分离,咱们发明了新的软件架构:MVP。虽然从感性的认识上来说,它是将Controller更名为Presenter,然而从理性的认识上来说,它在让视图和模型分离这件事情上作得更为决绝果断。经过图示咱们能够发现,视图和模型再也不发生直接联系,它们都经过Presenter相互联系,并且各个部分间的通讯都变成了双向流动。咱们能够很快意识到,如今全新的控制器即Presenter会变得愈来愈“重”,由于全部的逻辑都在这里,而视图会变得愈来愈“轻”,它再也不须要主动去获取模型提供的数据,它将被动地接拥抱变化,由于如今在视图里基本上没有任何业务逻辑。如今咱们能够预见,人类会在隔绝视图和模型这件事情上乘胜追击,人们会尝试让Controller/Presenter/ViewModel变得愈来愈臃肿,我想说的是,求它们在得知这一切真相时的心理阴影面积,咱们试图让每个模块各司其职、通力协做,结果脏活累活儿都交给了Controller/Presenter/ViewModel,我想说这件事情作的真是漂亮。数据库

MVVM模式示意图

  历史老是如此的类似,人类在做死的道路上匍匐前进,继续发扬更名的优良传统,这一次是Presenter被更名为ViewModel,在命名这件事情上,我认为程序员都是有某种强迫症因素在里面的,因此当你发现一个事物以一个新的名字出如今你的视野中的时候,一般它会有两种不一样的结局,第一,陈酒换新瓶,咱们贩卖的不是酒是情怀;第二,看今天的你我怎样重复昨天的故事,我这张旧船票还可否登上你的客船。幸运的是,MVVM相对MVP的确发生了些许改变,一个重要的特性是双向绑定,View的变化将自动反映在ViewModel中,而显然ViewModel是一个为View打造的Model,它能够容纳更多的普通的Model,所以从某种意义上来讲,ViewModel依然做为链接View和Model的桥梁而出现,它是对View的一种抽象,而抽象有两层含义,即数据(Property)和行为(Command),一旦你明白了这一点,ViewModel无非是一个特殊而普通的类而已,特殊是由于它须要实现INotifyPropertyChanged接口,普通是由于它继承了面向对象编程(OOP)的基本思想。编程

更像MVC的MVVM

  到如今为止,咱们基本上理解了MVC、MVP和MVVM这三者间的联系和区别,但是这样真的就是最好的结果吗?咱们首先来思考一个问题,即什么样的代码应该写在控制器里。好比咱们在对项目进行分层的时候,到底应该让控制器负责哪些任务?咱们可让Controller处理单独的路由,一样可让Controller参与视图逻辑,甚至咱们在编写Model的时候,咱们能够有两种不一样的选择,第一,编写一个简单的数据聚合实体,具体逻辑都交给控制器来处理,咱们将这种方式称为贫血模型;第二,编写一个持有行为的数据聚合实体,控制器在业务逻辑中调用这些方法,咱们将这种方式称为充血模型。因此,在这里咱们纠结的地方,实际上是选择让控制器更“重”仍是让模型更“重”,我曾经接触过1年左右的Android开发,我认为Android工程是一个相对符合MVC架构的设计,但是咱们不免会发现,做为控制器的Activity中的代码很是臃肿,由于咱们在这里须要和视图、模型关联起来,因此综合现有的这些软件架构思想,咱们发现模型和视图相对来说都是能够复用的,但是做为链接这二者的Controller/Presenter/ViewModel是很是臃肿并且难以复用的,因此我怀疑咱们是不是在真正的使用MVVM。安全

  我不知道MVVM架构正确的使用方法是什么样的,由于这是我第一次接触到这样一个新的概念,就如同不少年前,我在学校图书馆里看到的一本讲Web开发的书中描写的那样:当咱们不了解MVC的时候,咱们理所固然地认为经过文件夹将项目划分为Model、View、Controller,这样好像就是MVC啦。但是事实真的是这样吗?以我目前公司项目的状况而已,我认为它更像是使用了双向绑定的MVC,由于你常常能够在ViewModel中看到,某个属性的Get访问器中各类被if-else折磨的“脏”代码,而在ViewModel中我基本上看不到Model的身影,而且由于使用了Binding的概念严重弱化了ViewModel做为类的基本属性,所以它没有构造函数、没有初始化,咱们能够在Get访问器中看到各类硬编码,由于视图上的需求常常变更,因此当整个项目结束的时候,我本人是很是不肯意去看ViewModel这部分的代码的,由于项目上要求避免写Code-Behind代码,因此大量的事件被Command和UIEventToCommand代替,这样让ViewModel变得更“重”了。本来咱们但愿的是让这三者各司其职,结果如今脏活累活儿所有变成了ViewModel一我的的。虽然双向绑定能够避免去写大量赋值语句,但是我知道ViewModel心里深处会表示:宝宝内心苦。架构

  若是说WPF对技术圈最大的贡献,我认为这个贡献不在双向绑定,而是它真正意义上实现了设计和编程分离,咱们必须认可设计和编程都是一项创造性活动,前者趋向感性,然后者趋向理想,在没有实现这二者分离的时候,程序员须要花费大量时间去还原设计师的设计,但是对程序员来说,一段程序有没有界面设计在某些场合下是彻底不重要的,在没有界面设计的状况下,咱们能够经过单元测试来测试代码的可靠程度,相反地在有了界面设计之后咱们反而不容易作到这一点,因此你问我WPF对技术圈最大的贡献是什么,我会回答它解放了程序员,可让理性思惟去作理性思惟更适合的事情。我不太喜欢声明式编程,这里是指WPF中XAML这种继承自XML的标记语言,由于Visual Studio对XAML没有提供调试的支持,因此当你发现视图显示出现问题的时候,你很难分清楚是前台视图绑定出现错误仍是后台ViewModel出现错误,只要你输入符合XML规范的内容程序都会编译经过而非引起异常,由于它是用反射因此性能问题广为人所诟病,其次ViewModel中通知前台属性发生变化时须要使用OnPropertyChanged,该方法须要传入一个字符串类型的值,一般是指属性的名称,但是若是你定义了一个字符串类型的属性,当你在这里传入这个属性的时候,由于它是字符串类型因此不会引起编译错误,但是我以为这个东西仍是比较坑。mvc

委托与命令

  好了,如今我想说说WPF中的命令和委托,事实上在我计划写这篇文章前,我对这里无比好奇,可当我发现这东西的实质之后,我突然以为花费如此大的篇幅来说解这样一个概念,这是否是会显得特别无聊。咱们的项目上使用的是一个叫作MVVM light的框架,固然咱们没有使用它的所有功能,公司的前辈们很是猥琐地从这个开源项目中挑了些源代码出来,这里我不想说起关于这个框架自己地相关细节,由于我认为理解问题的实质比学会一个框架更加剧要。首先,WPF为每个控件都提供了一个Command的依赖属性,由于任何实现了ICommand接口的类均可以经过绑定的方式和前台关联起来,咱们这里对比下命令和路由事件的区别能够发现,路由事件必须写在Code-Behind代码中,而命令能够写在ViewModel里,因此直观上来说命令更加自由灵活。下面咱们以一个简单的例子来剖析这二者间的关系。框架

  咱们知道使用Command须要实现ICommand接口,因此实现起来是相对容易的,咱们这里继续沿用MVVM light中的RelayCommand这个名字:mvvm

public class RelayCommand : ICommand
{
    private readonly Action<object> m_execute;
    private readonly Predicate<object> m_canExecute;

    public RelayCommand(Action<object> execute)
    {
        this.m_execute = execute;
    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        this.m_execute = execute;
        this.m_canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        if (m_canExecute == null)
            return true;

        return m_canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter)
    {
        this.m_execute(parameter);
    }
}

咱们能够看到这里有两个重要的方法,Execute和CanExecute,前者是一个void类型的方法,后者是一个bool类型的方法。当咱们须要判断控件是否应该执行某一个过程的时候,CanExecute这个方法就能够帮助咱们完成判断,而Execute方法显然是执行某一个过程的方法,能够注意到经过委托咱们让调用者更加自由和灵活地传入一个方法,这是我喜欢这种设计的一个地方,由于个人一位同事就对普通的路由事件表示没法理解。

  这里须要说明的是CanExecuteChanged这个事件,这个和INotifyPropertyChanged接口中的PropertyChanged成员相似,是在当CanExecute发生变化的时候通知视图的,我对这里的理解是CanExecute自己就具有对某一个过程是否应该被执行的支持,但是遗憾的是在,在我参与的项目中,人们更喜欢声明大量的布尔类型变量来处理这里的相关逻辑,所以不管是对Property仍是Command而言,在ViewModel里都是看起来很是丑陋的代码实现。

  好了,如今对咱们而言,这是一个很是愉快的旅程,由于在完成对RelayCommand的定义之后,咱们绑定命令和定义命令的过程是很是简单的。除此之外,WPF提供了一个RoutedCommand类,该类实现了ICommand接口,我怀疑MVVM light中的EventToCommand正是经过这种思路实现了路由事件到命令的转换,由于只有RoutedCommand具有访问UI事件的能力,这里咱们仅仅提出问题,进一步的思考和验证咱们能够留到之后去作。下面咱们来看看如何声明和绑定命令:

public RelayCommand ClickCommand
{
    get
    {
        return new RelayCommand((arg)=> { MessageBox.Show("Click"); }); } }

显然这个ClickCommand将做为一个属性出如今ViewModel中,我选择了一个我最喜欢用的方法,或许这样看起来很是低端。但是在调试界面的过程当中,它要比断点调试更为直接和直观。当咱们的ViewModel中出现这样的只读属性的时候,直接在Get访问器中定义它的返回值彷佛是最直接有效的方案,可问题是Get访问器应该是很是“轻”的,由于大量业务逻辑的渗透,如今连这里都不能保留其纯粹性了吗?这让我表示很是郁闷啊。

<Window x:Class="WPFLearning.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window" Height="300" Width="300">
    <Grid>
        <Button Content="Button" HorizontalAlignment="Center" VerticalAlignment="Center" Command="{Binding ClickCommand }"/>  
    </Grid>
</Window>

如今你能够发现,委托和命令结合得很是好,当你发现这一切如此美妙的时候,回归本质或许是咱们最喜欢的事情,就像纯粹的你我同样,在这个世界上,咱们彼此装点着各自生命里美好的风景,执著而勇敢、温暖而明媚,那些周而复始的日子里,总能听到梦想开花的声音。

小结

  在这篇文章里咱们讨论了MVC、MVP、MVVM各自架构变迁的来龙去脉,由此咱们知道了软件设计中,一个典型的设计目标是让视图和模型分离,可咱们一样发现,带着这个目标去设计软件的时候,咱们基本鲜有更换视图的时候,虽然从理论上来说,全部的业务逻辑都是在ViewModel中,视图和模型应该是能够进行更换的,但是你告诉我,有谁会为同一个软件制做不一样的界面呢?难道咱们还能指望经过一个静态工厂,来为不一样的平台返回不一样的视图,而后理论上只要适配正确的控制器就能够实现软件对不一样平台的“自适应”,但是软件开发领域发展至今,最有可能提供完整跨平台方案的Web技术目前都没法知足这个需求,因此咱们是否应该去怀疑这个设计的正确性呢?一样的,以Java的SSH三大框架为表明的“配置文件”流派,认为应该将数据库的相关信息写在配置文件里,这样能够知足咱们随时切换到不一样数据库产品上的须要,但是你告诉我,这样的应用场景多吗?因此,技术自己的设计并无问题,咱们须要思考的是,是否应该被框架和架构束缚,说到底咱们是为了设计出更棒的软件产品,以此为目标,其实框架和架构更应该衍生为一种哲学意义上的思想,咱们想让每一行代码都充满智慧的光芒,它骄傲却不孤独,由于总有人理解它、懂它。