这是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:

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

那么接下来问题来了:点击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里来了

绑定元素属性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就如愿以偿的发生了改变:

用同样的方式去处理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

MVVM之旅(1)创建一个最简单的MVVM程序的更多相关文章

  1. 在VS中手工创建一个最简单的WPF程序

    如果不用VS的WPF项目模板,如何手工创建一个WPF程序呢?我们来模仿WPF模板,创建一个最简单的WPF程序. 第一步:文件——新建——项目——空项目,创建一个空项目. 第二步:添加引用,Presen ...

  2. 在C#/.NET应用程序开发中创建一个基于Topshelf的应用程序守护进程(服务)

    本文首发于:码友网--一个专注.NET/.NET Core开发的编程爱好者社区. 文章目录 C#/.NET基于Topshelf创建Windows服务的系列文章目录: C#/.NET基于Topshelf ...

  3. Win32 程序开发入门:一个最简单的Win32程序

    一.什么是 Win32 Win32 是指 Microsoft Windows 操作系统的 32 位环境,与 Win64 都为 Windows 常见环境. 这里再介绍下 Win32 Applicatio ...

  4. JNI编程(一) —— 编写一个最简单的JNI程序

    来自:http://chnic.iteye.com/blog/198745 忙了好一段时间,总算得了几天的空闲.貌似很久没更新blog了,实在罪过.其实之前一直想把JNI的相关东西整理一下的,就从今天 ...

  5. 【并发编程】一个最简单的Java程序有多少线程?

    一个最简单的Java程序有多少线程? 通过下面程序可以计算出当前程序的线程总数. import java.lang.management.ManagementFactory; import java. ...

  6. JNI编程(一) —— 编写一个最简单的JNI程序(转载)

    转自:http://chnic.iteye.com/blog/198745 忙了好一段时间,总算得了几天的空闲.貌似很久没更新blog了,实在罪过.其实之前一直想把JNI的相关东西整理一下的,就从今天 ...

  7. 创建一个Orchard Core CMS 应用程序

    开始使用Orchard Core作为NuGet软件包 在本文中,我们将看到使用Orchard Core提供的NuGet包创建CMS Web应用程序是多么容易. 你可以在这里找到Chris Payne写 ...

  8. SAP Cloud Platform integration上创建一个最简单的iFlow

    登录SAP CPI控制台,点击这个铅笔图标进入工作区域: 选择一个已经存在的content package: 在这个content package里创建一个新的iFlow: 默认生成的iFlow模型如 ...

  9. 如何用Unity创建一个的简单的HoloLens 3D程序

    注:本文提到的代码示例下载地址>How to create a Hello World 3D holographic app with Unity 之前我们有讲过一次如何在HoloLens中创建 ...

随机推荐

  1. CTF---安全杂项入门第三题 这是捕获的黑客攻击数据包,Administrator用户的密码在此次攻击中泄露了,你能找到吗?

    这是捕获的黑客攻击数据包,Administrator用户的密码在此次攻击中泄露了,你能找到吗?分值:30 来源: 2014sctf 难度:难 参与人数:3918人 Get Flag:384人 答题人数 ...

  2. [hdu5632][BC#73 1002]Rikka with Array

    点开BC发现今晚没比赛..然后似乎上一场有数位DP?...(幸好我没去 一开始被BCDcode那题的思路带歪了..后来发现得把n转成二进制才能搞TAT 题目大概是要求一种类似逆序对的鬼东西: 有一个长 ...

  3. BZOJ3930: [CQOI2015]选数

    题目:http://www.lydsy.com/JudgeOnline/problem.php?id=3930 容斥原理. 令l=(L-1)/k,r=R/k,这样找k的倍数就相当于找1的倍数. 设F[ ...

  4. hdu_4463(最小生成树)

    hdu_4463(最小生成树) 标签: 并查集 题目链接 题意: 求一个必须包含一条路径的最小生成树 题解: 把那条边初始化成0 保证这条边一定会被选 #include<cstdio> # ...

  5. poj_3281Dining(网络流+拆点)

    poj_3281Dining(网络流+拆点) 标签: 网络流 题目链接 题意: 一头牛只吃特定的几种食物和特定的几种饮料,John手里每种食物和饮料都只有一个,问最多能够满足几头牛的需求(水和食物都必 ...

  6. a*b(mod m)的实现过程

    /*a*b (mod m) 的实现过程*/ /*当a,b很大的时候mod m就会产生溢出, 故运用乘法原理转换为加法求解*/ LL multi(LL a, LL b, LL m) { LL exp = ...

  7. MySQL数据库全备

    #function:MYSQL自动全备 #version:1.0.0 #author:wangyanlin #date:2017/08/03 #---------------------------- ...

  8. Windows系统下文件的概念及c语言对其的基本操作(丙)

  9. 在Linux上如何查看Python3自带的帮助文档?

    俩个步骤: 在Linux终端下输入: ortonwu@ubuntu:~$ pydoc -p 8000 pydoc server ready at http://localhost:8000/ 打开浏览 ...

  10. 防止ajax重复提交

    在jquery中防止ajax重复提交