MVVM之旅(1)创建一个最简单的MVVM程序

这是MVVM之旅系列文章的第一篇,许多文章和书喜欢在开篇介绍某种技术的诞生背景和意义,但是我觉得对于程序员来说,一个能直接运行起来的程序或许能够更直观的让他们了解这种技术。在这篇文章里,我将带领大家一步一步创建一个最简单的MVVM程序,程序虽然简单,但是却涵盖了MVVM的基本要素,对于那些还不是很了解MVVM的读者来说,相信这会是一个很好的入门。

程序的功能非常简单:两个按钮一个文本框,点击某个按钮就把某个按钮上的文字显示到文本框里。

传统做法的问题

对于如此简单的问题,传统的做法就是一句话的事,双击Button,在xaml.cs文件的事件响应函数里写下下面这样一行代码就行了:

this.textBox1.Text = button1.Content.ToString();

这种做法很简单,但是却暴露了一个很严重的问题:this.textBox1是对视图元素的一个强引用,这样的代码把视图和逻辑完全耦合在了一起(如果没有textBox1这个具体的视图对象实例,这行逻辑代码根本编译不过去,逻辑离不开视图,这就耦合了)。这样的代码在小软件里没啥问题,但是当软件变大变复杂的时候问题就来了:在大型软件开发里,大家都是相互分工合作,各自负责自己的模块,有人负责界面设计,有人负责后台逻辑,如果代码这样写,那美工的新版界面还没有画好的时候,我后台的逻辑岂不是不能写不能测试了? 

视图和逻辑分开早已是共识

把软件的视图界面和逻辑分开并不是MVVM的发明,上世纪80年代MVC就把视图层和逻辑层分开(加上数据层,构成了经典的三层架构),后来的MVP在MVC的基础上做了改进,使得程序之间的耦合性再次降低,微软以MVP为基础,考虑到WPF的特性,推出了纯数据驱动的MVVM框架。

这里要特别提一下数据驱动,MVVM让我们的编程方式从原来的消息驱动、事件驱动转成了更加高效的数据驱动,这是跟MVC、MVP完全不一样的。也因此,MVVM里的ViewModel并不等同于在MVC和MVP里做逻辑处理的Controller和Presenter,它更像一个数据格式化器,它的任务就是把来源不同的各种数据进行处理,然后按照一定的格式提供给View。 

MVVM的做法

既然MVVM是继承了MVC、MVP这种经典的三层架构的风格,那么它肯定将视图层(V-View)和逻辑层(VM-ViewModel,这里只是借鉴了逻辑层这样经典的一个概念,把ViewModel翻译成逻辑层并不合适,但是业务逻辑一般确实也是在这里做的)做了解耦,因为我们这个例子非常小,所以暂时不涉及数据层(M-Model)。

我们先建一个WPF的项目,项目里添加Views和ViewModels两个文件夹。顾名思义,Views文件夹里存放所有的View,ViewModels文件夹里存放对应的ViewModel:

QQ截图20180206091702

然后我们将两个按钮一个文本框放到ChildWindow里:

QQ截图20180206092119

那么接下来问题来了:点击Button并且改变TextBox里内容这个事情,如果不能在ChildWindow.xaml.cs里通过响应Button的click事件来完全,那要怎么做呢?或者说的再简单一点,不准你在xaml.cs文件后面写代码,你要怎么实现这个事情?(Xaml文件代表的是我们的视图,xaml.cs里写代码非常容易造成视图和逻辑的耦合,如果我们想彻底解耦视图层和逻辑层,那么直接让xaml纯负责视图,我的逻辑部分完全写在另外的地方是非常简单有效的办法。Android就是这么干的,而且更彻底,Android开发里使用纯XML文件代表视图,它压根就不提供xml.cs这种东西让你写代码,你想要使用视图里的元素,你得在其他地方使用findViewById来找)。

MVVM给的答案就是:绑定(Binding)+命令(ICommand)。 

添加绑定(Binding)

MVVM把View放在Xaml文件里,把逻辑放在ViewModel里,然后通过绑定让指定的View和ViewModel关联在一起。你要处理什么业务逻辑都在ViewModel里写,业务逻辑处理完了要更新View的时候也不是直接用“this.xxxView.某属性=xxx”这样的句式来更新(其实你想这样更新也做不到,因为ViewModel为了和View解耦,里面根本就不会持有View对象的引用)而是通过更改ViewModel里和View绑定的相关属性来修改View。

把ChildWindow和ChildWindowViewModel绑定在一起很简单,在ChileWindow.Xaml里设置DataContext就行了:

<Window x:Class="MVVMDemo.Views.ChildWindow"
        ...
        xmlns:vm="clr-namespace:MVVMDemo.ViewModels">
    <Window.DataContext>
        <vm:ChildWindowViewModel/>
    </Window.DataContext>

这样做了之后View和ViewModel就绑定在了一起。不过,因为我们在点击Button之后要改变TextBox的显示内容,所以我们还得把TextBox的Text属性跟ViewModel做绑定,我们先在ChildWindowViewModel里建一个TextBox1Text属性用来给TextBox的对象做绑定:

public class ChildWindowViewModel
{
    public string TextBox1Text { get; set; }
}

然后把textBox1的Text属性和它绑定在一起:

<TextBox Name="textBox1"  Text="{Binding TextBox1Text}" .../>

 添加命令(ICommand)

绑定工作做好了接下来就要添加命令了。因为我们不能直接在xaml.cs文件里写click事件的响应,所以响应点击按钮这个事情是通过命令(ICommand)来实现的。

我们先在ViewModel里添加一个ICommand属性:

public ICommand Button1Cmd
{
    get
    {
        return new DelegateCommand((obj) =>
            {
                  //button1点击之后要做的事情写在这里
    
            });
    }
}

然后同样把这个ICommand属性和button1的Command属性绑定在一起:

<Button Content="Button1" Command="{Binding Button1Cmd}" .../>

这样做了之后只要点击button1,就会自动执行Button1Cmd里的代码。在这个Button1Cmd属性里,我们看到有个DelegateCommand类,这是在MVVM使用频率超高的一个基础类。因为ICommand只是一个接口,DelegateCommand帮助我们做了一些在MVVM里非常基础公共的事情,使得我们可以直接在Button1Cmd里如此简洁的写命令代码(说实话,微软没把这个类写进类库里我都感觉奇怪)。

DelegateCommand的代码如下(文章末尾的源代码里还提供了它的泛型版本):

public class DelegateCommand : ICommand
    {
        private Action<object> executeAction;
        private Func<object, bool> canExecuteFunc;
        public event EventHandler CanExecuteChanged;

        public DelegateCommand(Action<object> execute)
            : this(execute, null)
        { }

        public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
        {
            if (execute == null)
            {
                return;
            }
            executeAction = execute;
            canExecuteFunc = canExecute;
        }

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

        public void Execute(object parameter)
        {
            if (executeAction == null)
            {
                return;
            }
            executeAction(parameter);
        }
    }

 测试命令

我们删掉MainWindow,把App.xaml里的StartUri设为ChildWindow的路径,让程序运行的时候直接启动ChildWindow:

<Application x:Class="MVVMDemo.App"
             ...
             StartupUri="Views/ChildWindow.xaml">

在DelegateCommand里添加一行弹出消息提示框的代码:

return new DelegateCommand((obj) =>
                    {
                        //button1点击之后要做的事情写在这里
                        System.Windows.MessageBox.Show("button1 click!");//测试代码
                    });

点击button1,看到了如下弹出的消息框,证明Button绑定的命令确实传到ViewModel里来了

QQ截图20180206105524

 绑定元素属性TextBox1Text

那我们接下来在这里去修改TextBox1Text的值,因为TextBox1Text这个属性已经和ChildWindow里的textBox1的Text属性做了绑定,所以按照我的想法,如果我在ViewModel里修改了TextBox1Text的值,textBox1显示的数字就会跟着改变。

按照这个思路我们添加了如下的代码:

return new DelegateCommand((obj) =>
                    {
                        //button1点击之后要做的事情写在这里
                        //System.Windows.MessageBox.Show("button1 click!");//测试代码
                        this.TextBox1Text = "button1 click!";
                    });

再次运行,点击button1,结果却什么事情都没有发生,并没有出现我们期待的textBox1里出现“button1 click!”的字样,为什么呢?明明确实执行了这行代码,View和ViewModel之间的绑定也确实做好了,TextBox1Text的改变为什么不能自动改变textBox1的值?

答案是这样的:我们虽然把ViewModel的属性跟View元素的属性做了绑定,如果想让ViewModel里的属性发生变化之后View里对应的元素也跟着变,你得手动通知它。

为什么需要我们手动去通知,微软为什么不把这种东西都做到框架里面去?

你想啊,View的界面里有这么多元素,每个元素都有这么多属性,而我需要改变的属性只有那么几个,我不能因为我要改变这几个属性而把所有的属性都附加上这种功能把,这样太浪费资源了。另外,自己去手动通知代码也非常简单,都是可以重复利用的。

 添加通知INotifyPropertyChanged

因为每个属性要通知界面都要实现这个通知接口,所以可想而知,这是一个要重复做很多次的事情。为了让我们以后更加省心,我们把这个通知接口的实现放到基类ViewModelBase里去,让所有的ViewModel继承这个基类就行了

public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

然后我们在ChildWindowViewModel继承ViewModelBase,改写一下TextBox1Text属性:

private string textBox1Text;
public string TextBox1Text
{
    get
    {
        return this.textBox1Text;
    }
    set
    {
        this.textBox1Text = value;
        RaisePropertyChanged("TextBox1Text");
    }
}

我们在TextBox1Text的set里添加了RaisePropertyChanged("TextBox1Text");这样一行,这就是告诉系统,如果我这个属性发生了改变,就去通知界面里一个叫“TextBox1Text”的属性(不过他只负责通知到位,通知到了之后你要做什么它就不管了)。

再次运行程序,点击button1,我们发现textBox1就如愿以偿的发生了改变:

QQ图片20180206112528

用同样的方式去处理button2,效果一样的。

 结语

至此,我们这个全世界最简单的MVVM程序的功能就已经都实现了。通过绑定和命令实现了一个最简单却非常具有代表性的操作:界面点击操作,后台处理逻辑,处理好了以后把结果更新在界面里,也抽象出来了DelegateCommand和ViewModelBase两个通用类。它很好的解耦了视图和逻辑:大家可以看到,我们的ChildWindowViewModel里面没有任何和View相关的代码,因此完全可以单独拿来出测试;我们的ChildWindow.xaml.cs文件里没有一行代码,ChildWindow完完全全就是一个视图界面,你也可以对他单独操作。

而且更重要的是:我的ChildWindowViewModel只要第一次去设置好和ChildWindow绑定的属性,以后就再也不用跟View打交道了,我以后所以对View的操作都变成了对ViewModel里属性的操作,我只要知道我要写的逻辑最后要赋值给那个属性就行了,至于那个属性最终会以什么样的形式绑定呈现在界面上,我完全不关心。这不是程序员梦寐以求的事情么?

对于美工来说一样解脱了,以前一个大的项目组里虽然有程序员,也有专门的美工,但很多时候的工作是这样的:程序员说这里需要一个蓝色的按钮,美工就去切一个按钮给程序员,程序员把这张图片设置成按钮的背景,接着要信息显示的背景图片又得找美工要,然后自己写程序把图片样式颜色都调好。但是现在却可以变成这样:项目经理说这个View要显示一个人的各种具体信息(年龄性别名字等等等之类的),然后美工可以拿起Blend这样的工具,按照自己的想法把这整个View的界面画好,然后直接就向贴纸一样贴在程序员写的ViewModel里,程序员什么都不用改,指定一下DataContext和绑定属性就可以直接用了,这样的合作多么畅快人心!

另外,MVVM虽然是微软为了WPF量身定做提出来的,但是它的思想却非常具有启发性,它通过绑定让视图和逻辑层之间的解耦比MVP还彻底,所以现在不止WPF,Android、IOS、前端开发都在研究MVVM。但是毕竟MVVM是微软为了WPF量身定做的,所以总的来看,还是WPF对MVVM的实现最为自然简洁优雅。深入了解MVVM的思想和实现对提高WPF的编程水平有巨大的帮助,如果还是使用MFC、Winform时代的思想来写WPF程序,那就真的是白白浪费了WPF这个如此先进的技术,有种用屠龙刀在切牛肉的既视感。

文章代码下载地址:MVVMDemo.rar

posted @ 2018-02-06 13:57  Zeek  阅读(6622)  评论(52编辑  收藏  举报