WPF中的MVVM模式
周银辉
"设计模式"这样的话题似乎快被园子里的兄弟们写透了, 从简单的工厂到 MVC, MVP. 而关于MVVM似乎谈论得相对少些, 今天简单地说说. 值得声明的是: 这里仅仅谈论得是自己对别人发明的东西的一些理解, 可能有所偏误, 望理解. 另外, 搜索了一下,园子里 "clingingboy" 和 "高阳"大哥也谈到了这个模式, 大家不妨参考一下.
在阅读以下内容以前,建议你对这些内容有所了解: WPF, MVC, MVP, MVVM. 关于MVVM语法层面的内容请参考这里: http://msdn.microsoft.com/en-us/magazine/dd419663.aspx
1, 前提
可以说MVVM是专为WPF打造的模式, 也可以说MVVM仅仅是MVC的一个变种, 但无论如何, 就实践而言, 如果你或你的团队没有使用"Binding"的习惯, 那么研究MVVM就没有多大意义.
另外,个人觉得, 使用Command以及打造一种合理的简化的方式去使用Command也与使用Binding一样重要.
2, 诞生
为了解决现实世界中的问题,我们需要将现实世界中的事物加以抽象, 然后得到了Domain Object, 无论贫血的还是富血的, 我们都可以简单地把他们归结为"由现实世界抽象出来的模型", 也就是我们的model, 也就M-V-VM中的"M".
但其无法与我们的用户进行交互, 所以, 我们需要为其创建一个界面(视图, View), 该视图可以与用户输入设备进行交互, 这很棒, 但问题是如何将View与我们的model关联起来? Binding便可以发挥作用了, 比如视图上的某一个文本框中的文本和Model中的"用户名"关联起来, 用户便可以通过操作该文本框来访问和修改Model的"用户名"了.
这是极其简单的情况, 但实际编程时我们发现, Model中的属性(与方法)往往不那么容易与View中的界面控件关联起来, 比如, "类型不匹配": 界面控件所需要的类型与模型中属性提高的类型不匹配. "需要额外操作": 模型中的数据需要经过一些额外的处理才能传给视图,反之亦然. 此时, 我们意识到View似乎需要一个"Helper"类来处理一些额外工作.
这个helper所包含的代码可以放在除了Model外的很多地方(我们现在不考虑贫血富血之类的争论), 比如View中, 记得自己刚学习窗体程序开发时就是这么干的, 将绝大多数处理逻辑放在那个所谓的CodeBehind中. 后来,正如大家在各种设计模式书籍中所看到的一样,为了将View和Model剥离开来,实现view可替换(比如你可以讲自己精心设计的软件同时运行于窗体程序,Web甚至Mobile上), 便有了MVC. 有了MVC以后似乎就开始滋生M-V-XXX之类的争论与变种模型, 比如MVP以及这里的MVVM,甚至MVP也有着Supervising Controller与Presentation Model两种方式. 但主要围绕两个问题,一是model与view之间的关系, 完全隔离的?单向的还是双向的? 二是这个"XXX"需要完成哪些功能,简单流程调度?复杂规则处理? OK,这些争论都没有关系, 是否采用某种模式取决于你的开发所处的环境(比如语言特性,框架特性)以及你的业务特性以及所面临的主要变化点等等.
但与MVC,MVP所不同的是,MVVM的引入不仅仅是技术上的原因(解除耦合应对变化等老生常谈),另外一个很大原因是:软件团队开发方式的改变.如果你做过一段时间的WPF项目开发的话,你可能会有比较明显的感觉:在View层打造上,如何分配程序员和美工的工作.在继续阅读之前,大家可以看看我以前的一篇文章"在UI Designer与Developer之间". 以前我们团队采用的便是"集成模式", 我便兼职了其中的"Integrator"角色.这还不错.但说实在的,这仅仅是一个在特殊情况下不得已而为之的暂时方案,所以我们付出了很大的努力开始转向"收割模式"了,要转向这个模式,至少需要两个基本条件:
(1)你拥有能够熟练运用Blend等工具能为程序员输出XAML的美工, 他专注于纯粹的UI/UE, 另外他还必须具有一定的"程序员"思维.以便输出的东西能很好地作为程序的一部分而运转起来,而不是仅仅"看上去"是那样的.
(2)你需要能够脱离View层但仍能编写出高质量代码的程序员.
幸运的是, 我们在努力创造条件1,并取得了很好的效果.(你可以招一个具有Flash脚本编写经验的并且有极大的学习热情的美工人员, 并对他进行Blend的相关培训). 而MVVM模式为我们实现第二个条件提供了极大的便利. 为什么MVC/MVP模式不行而MVVM可以呢? 很简单, 在MVC和MVP模式中, View层都具有很多代码逻辑, 开发View层的是程序员, 虽然UI/UE团队会做很多工作, 但这个层的"实现者"仍然是程序员. 在以前的开发中,其工作得很好, 而在WPF开发中程序员对View层的展现显得力不从心了,美工(指符合上面条件1的美工)虽然很擅长, 但他会说"可惜我不会程序".于是, 我们需要一种方式将View层的代码逻辑抽取出来,并View层很纯粹以便完全让美工去打造它.相应地, 需要将View层的相应逻辑抽取到一个代码层上,以便让程序员专注在这里.
回想一下, 我们只所以要在View(Xaml)背后写一些代码(C#), 无非是想传递一些数据以及传递数据时的数据的处理或在用户与界面控件进行交互时执行一些操作, 最简单的例子是在MVC中当界面发生交互时View去调用Controler中的某个方法, 以便将该操作的相应"指示"传递到"后台"去. 在以前的技术中, 这样的"衔接性"的代码是必须的. 而在WPF中, 则可以通过另外的技术来进行层与层之间的"衔接", 这就是"Binding" 和"Command", 以及稍后我们会提到的"AttachBehavior". 通过Binding, 我们可以实现数据的传递; 通过Command, 我们可以实现操作的调用.(AttachBehavior的作用稍后再谈). Binding和Command是可以写在XAML中的, 这样看来XAML后面对于的CS文件可以被完全抛弃或不予理会了. 这样的XAML文件正是美工所需要的. 而这些对于Binding以及Command的定义描述以及其他相关信息的代码应该放在那里呢, 当然不是View, 更不是Model, 是"ViewModel". ViewModel是为这个View所量身定制的, 它包含了Binding是所需的相关信息,比如Converter以及为View的Binding提供DataContext, 它包含了Command的定义以便View层可以直接使用, 另外,它还是一个变种的Controler, 它得负责业务流程的调度.
于是, 便有了这副图, 然后, 正如"时势造英雄"所言, MVVM就诞生了.
3, ViewModel 与 单元测试
如果你是一名正在使用MVVM模式打造软件的程序员, 那么我劝你尽快忘掉View. 你所面对的是这样一个模式"UnitTest-ViewModel-Model"(这并非一个模式, 仅仅是我为阐述观点而暂时如此表述的).
记得曾经有一个Model-View-AbstractView模式, 而MVVM中的VM实际也是一个AbstractView: the abstraction of view. 它是一个抽象的View, 具有一个View的灵魂,而不具备相应的可视化控件而已. 所以对于程序员而已, 打造这样一个抽象的VM就可以认为是完成View层的打造了.而当美工完成无数控件组成的实际的View后, 我们就可以用Binding和Command这样的黏合剂将这个抽象的View和实际的View黏合在一起了.
那么在黏合之前, 我们怎么知道自己的VM是否正常工作呢? 单元测试!
在说明对于ViewModel进行单元测试的重要性之前, 送给大家一句话: "View and Unit Test are just two different types of ViewModel consumers" (Josh Smith). 如果我们将ViewModel看作生产者, 那么View和Unit Test都是具有同等地位的消费者而已. 并且UnitTest相比于View而言具备更大的消费能力. 或者你可以简单的认为View也仅仅是一种不太推荐的测试方式而已. 所以要实施好这个模式, 那么对ViewModel的单元测试就是必须的了,并且这个测试要不依赖于任何UI控件. (那么不是不对应ViewModel的开发是不是就应该通过测试来驱动了?TDD?)
4, AttachBehavior
一般情况下利用Command, Binding, AttachProperty等WPF特性, View和ViewModel之间能配合工作得很好. 假设我们有一个Button, 当该Button被点击的时候我们要完成一些操作, 很简单, 将该操作封装成一个Command并绑定到该Button上就可以了, 但如果我们要在Button被Load的时候执行另外一些操作呢? 由于Button没有直接被Load事件所触发的Command, 所以不能使用Command了. 不能直接将Load事件处理器写在Button所在的Xaml所对应的CS文件里, 这和我们刚才对MVVM的设计是相矛盾的. 一个不太好的方案是继承一下Button, 并撰写一个由Load所触发的Command, 这可行, 但明显不好. 正如一个控件没有某个属性并且在不继承的情况下而采用AttachProperty一样, 我们可以采用AttachBehavior. AttachBehavior不是WPF特性, 它仅仅是一个最佳实践, 一个Pattern. 关于AttachBehavior语法如何书写, 请参考 : http://www.codeproject.com/KB/WPF/AttachedBehaviors.aspx