说不尽的MVVM(2) – MVVM初体验

时间:2021-05-09 14:25:09

知识预备

阅读本文,我假定你已经具备以下知识:

  • C#、WPF基础知识
  • 了解Lambda表达式和TPL
  • 对事件驱动模型的了解
  • 知道ICommand接口

发生了什么

说不尽的MVVM(2) – MVVM初体验说不尽的MVVM(2) – MVVM初体验说不尽的MVVM(2) – MVVM初体验

某程序员接到一个需求,编写一个媒体渲染器,效果如图,功能就是渲染媒体并在UI上面报告进度,看看他是怎么实现的。

事件驱动模型的实现

首先是界面

说不尽的MVVM(2) – MVVM初体验

三个TextBlock分别负责显示状态、进度和百分号,还有一个按钮让用户启动渲染操作。

说不尽的MVVM(2) – MVVM初体验

点击按钮时,异步调用Render方法。

说不尽的MVVM(2) – MVVM初体验

在Render方法里面更新UI,模拟渲染操作,更新进度。

看起来不错,功能也正确,他正开心呢,突然电话响了。。。

为毛改需求啊!!!

客户最烦了,他居然嫌按钮太土,要用菜单,还说数字不够直观,要用进度条显示。没办法,谁让客户是上帝呢,改吧。

说不尽的MVVM(2) – MVVM初体验

很快他就做出了这样的界面,但是这下惨了,改了界面后,后台代码那是"全国山河一片红"啊

说不尽的MVVM(2) – MVVM初体验

那些什么renderButton progressDisplay 全挂了,无奈,只好重新写。

细数事件驱动模型的三宗罪

第一宗罪:逻辑代码是无辜的!!!

后台代码中频繁引用UI上的控件,导致逻辑与UI的耦合过于紧密,一旦UI发生改变,后台的逻辑代码就会躺枪,接着我们的程序员就会躺枪,逻辑根本没变啊,为毛要重写?

第二宗罪:太多与数据无关的代码

我们看到,上面的代码很大一部分在更新UI,虽然这是一个示例程序,处理业务逻辑的代码很少,但也说明一个问题,逻辑代码中确实存在与数据无关的代码,这些本来应该由UI来完成的工作,却跑到了逻辑代码里面,造成一种混乱。

第三宗罪:Dispatcher很忙

在工作线程中更新UI每次都要调用UI的Dispatcher,无论从代码还是性能方面都说不过去。

用MVVM升级你的程序

在了解MVVM之前,我们不妨先体验下MVVM

发现UI的本质

其实我们发现,无论UI怎么变,这个程序总是在完成这么一个功能,就是需求描述里面的:渲染媒体并在UI上面报告进度。因此我们可以对UI建模:

说不尽的MVVM(2) – MVVM初体验

MVVM light toolkit

MVVM light 是MVVM框架中个人认为比较不错的一款,开源(http://mvvmlight.codeplex.com/ ),本文以他为例,对MVVM进行一个简单的介绍。首先理清三个词,MVVM:一种设计模式,MVVM light toolkit:MVVM的一种实现,MVVM light:这个只是MVVM light toolkit的名字啦。

OK,我们先去获取MVVM light.

说不尽的MVVM(2) – MVVM初体验

NuGet里面搜一下就可以搜到,第一个包含了类库和一些工具,比如code sinppet,第二个是单纯的类库,第三个是可移植类库(Portable Class Libraries),这里我们选择第三个。

说不尽的MVVM(2) – MVVM初体验

我们把后台代码去掉,UI恢复最初的状态。,开始上MVVM模式。

建立View Model

说不尽的MVVM(2) – MVVM初体验

根据上面的建模,我们可以设计出这么一个类

说不尽的MVVM(2) – MVVM初体验

类里面的Render方法模拟渲染操作

说不尽的MVVM(2) – MVVM初体验

由于正在进行渲染操作时,不能启动另一个操作,因此要有一个方法判断当前是否能进行渲染操作

将UI与数据关联

说不尽的MVVM(2) – MVVM初体验

把我们建立好的ViewModel作为MainWindow的DataContext,然后再UI里面用数据绑定。

说不尽的MVVM(2) – MVVM初体验

默认情况下,WPF Data binding的source就是自身的DataContext ,因此我们只需要为binding 指定Path。

命令

说不尽的MVVM(2) – MVVM初体验 

Button的Command在ViewModel 里面是一个RelayCommand(MVVM light对 ICommand 的一种实现),以

ICommand的形式暴露出来。

说不尽的MVVM(2) – MVVM初体验

我们知道,ICommand接口有两个方法,Execute和CanExecute。因此在初始化RelayCommand的时候,指定这两个方法。

现在启动程序,点击按钮,后面的数据已经开始处理了,但我们发现两个问题:UI上面的进度不会更新,按钮在正在处理时没有变灰。下面我们来解决这两个问题。

将数据的更改通知界面

虽然ViewModel里面的数据更新了,但是UI上没有变化,原因是UI并不知道后面的数据发生了改变,因此,在数据改变后,我们应该立即通知UI,数据变了。

说不尽的MVVM(2) – MVVM初体验

说不尽的MVVM(2) – MVVM初体验

这里我们可以把MVVM light的VoewModelBase作为ViewModel的基类,在ViewModelBase里面有一个RaisePropertyChanged方法,这个方法可以告诉data binding,数据更新了,我们只需要在属性的set分支里面调用就可以了,参数是属性名(这方法在C#5.0下不需要参数)。

让按钮变灰

我们知道,命令能否执行与IsProcessing是有关联的,那么,当IsProcessing改变的时候,通知一下。

说不尽的MVVM(2) – MVVM初体验

因为我们会在别的线程中更新这个属性,因此我们通过DispatcherHelper来执行Command的RaiseCanExecutrChanged方法。

说不尽的MVVM(2) – MVVM初体验

DispatcherHelper使用之前要初始化,本例在ViewModel的构造函数中进行。

等等,我不要True和False

bool 类型直接绑定到界面会显示他的ToString()结果,显然这不是我们想要的,但我们可以做一个转换。

说不尽的MVVM(2) – MVVM初体验

我们新建一个类,实现 IValueConverter 接口,由于只是一个单向的转换,因此只需要实现Convert。

说不尽的MVVM(2) – MVVM初体验

然后在UI上面使用。

说不尽的MVVM(2) – MVVM初体验

这样,我们就可以得到我们想要的结果了。

再也不怕改UI了

说不尽的MVVM(2) – MVVM初体验

现在把UI改成新的UI,ViewModel根本不需要动,只需要把Data binding再写一次就可以了。

说不尽的MVVM(2) – MVVM初体验

你可能会有的疑问

为什么把ViewModel放到DataContext?

为数据绑定提供数据源的方法有很多种,放到DataContext是一个用的比较多的方法。

这里给一个MSDN的参考资料:How to: Make Data Available for Binding in XAML

RaisePropertyChanged的实现原理是什么?

ViewModelBase实现了INotifyPropertyChanged接口,通过PropertyChanged事件通知Data binding(下一篇会讲述)。

貌似做了很多额外工作,就为了那点灵活?

为这样一个小项目上一个框架,确实是过度设计了,但在一个大一些的项目中,这种UI和逻辑的解耦带来的便利和灵活远远超过你的付出。