写点什么

MVVM 启示录

2012 年 4 月 03 日

细究 MVVM

熟悉 WPF 或 Silverlight 的同学应该不会对 MVVM 模式感到陌生了,它把应用程序划分成视图、视图模型和模型三层,如图 1 所示:

图 1

表面上,这个层次结构还蛮清楚的,但如果你细究每层应该包含什么,事情就没那么简单了。

视图应该是最容易理解的一个部分了,它通常是指用户可以看到的界面,一般都是通过 XAML 代码来实现的。但是,XAML 代码并不能实现一切想要的效果,这个时候很多人会把目光投向代码隐藏文件,在里面通过事件处理程序实现某些效果。那么,究竟什么样的代码可以放在代码隐藏文件里?这些代码是否包含原本应该放在视图模型里的代码?这些效果是否还有其他途径可以实现?

视图模型从字眼上看应该是视图的抽象,这意味着每个视图都应该有一个对应的视图模型,这也是我们经常看到的做法。不过,有人对此表示反对,他们认为视图和视图模型不是一一对应的关系,整个应用程序应该有一个主要的视图模型,各个视图将会绑到这个主要的视图模型上,同时有一些次要的视图模型,用来表示诸如配置等辅助方面。那种做法才是正确的?视图模型里面又该包含什么样的代码?

最后是模型,大多数人对它的一个共识就是,它会包含具体的数据,这些数据最终会显示到用户界面上。问题是,它到底是简单的 POCO 还是业务逻辑的复杂类型?我们是否应该在这里放置验证逻辑?是否允许它和视图直接绑定,还是需要另外创建对应的包装类?如果使用 LINQ to SQL 存储数据的话,模型里的类是否就是 LINQ to SQL 的实体类?如果需要访问 Web Service 获取数据,模型里的类和添加 Web Service 时自动生成的类又是什么关系?

哇,问题还真不少啊!你是否曾经遇到这些问题?你对它们有什么想法?诚然,这些问题没有唯一的标准答案,它们都是开发者在具体实践中提炼和总结出来的智慧,但它们通常只针对于特定的应用场景,或者说,它们是为了满足某些需要而产生的。举个例子吧,有些人可能会觉得添加 Web Service 时自动生成的类和他们要创建的模型类基本上是一致的,为了避免重复劳动,他们选择直接使用那些自动生成的类,这种做法一般不会出现问题,直到由于需求的变更,模型不再和 Web Service 对应起来,但此时应用程序的其他部分已经通过这些自动生成的类和 Web Service 紧密耦合起来了,修改应用程序就可能变得非常困难。相反,如果应用程序的功能比较单一、专注,Web Service 的接口也比较稳定,那么特意为那些自动生成的类创建一组一模一样的模型类显然增加劳动成本,埋下潜在的维护问题。

最简单的实现

假设我经常去图书馆借书,我需要一个应用查看所有图书的归还日期,如图 2 所示:

图 2

这是一个非常简单的应用,从 MVVM 模式的角度来看,图 2 所展示的用户界面就是视图了,而视图模型和模型也都非常简单,分别为图 3 的 MainViewModel 类和 Book 类:

图 3

页面的 ListBox 控件将会绑到 MainViewModel 的 Books 属性,书名将会绑到 Book 的 Title 属性,而归还日期则绑到 Book 的 DueDate。

到目前为止,一切都非常自然顺畅,直到我对它提出两个新的需求:

  • 图书列表根据归还日期从小到大排序,即最先要还的书拍在最上面。
  • 今天和明天要还的书字体使用强调色。

这两个新的需求都非常合理。第一个需求属于页面的抽象逻辑,不与页面的任何控件挂钩,这种需求一般会在视图模型里面实现,具体地就是在 MainViewModel 的构造函数里初始化 Books 属性时进行升序排序。

至于第二个需求,它涉及到具体的 TextBlock 控件以及对 Book 类的 DueDate 属性的二次处理,原则上不应该在 Book 类里面实现,根据个人偏好,这个需求有两种不同的实现方式:

  • 创建一个 ItemViewModel 类,包装 Book 类并暴露相关属性,同时提供一个 Foreground 属性用于和 TextBlock 控件的对应属性绑定,Foreground 属性可以在初始化 ItemViewModel 时根据 Book 的 DueDate 属性计算。
  • 通过转换器实现相同的效果。

有人说,使用 MVVM 模式可以消除转换器的需要,是的,任何时候当你需要一个转换器,你都可以通过创建包装类并提供额外的属性获得相同的效果,但我们不应该把这个问题绝对化,转换器的存在价值体现在可以在不同的绑定关系上重用相同的逻辑,而且更符合 Expression Blend 用户的使用习惯。

看到这里,有些同学可能会问,如果用户要求同时提供根据图书标题和归还日期两种排序方式呢?每当我们遇到一个新的需求时,请不要马上动手实现或者考虑如何实现,应该先想想用户为什么有这样的需求。根据归还日期进行排序这个需求对应着帮助用户避免逾期归还所受的惩罚,但根据图书标题排序呢?很多时候,我们会想当然地认为用户需要某些功能,而忽略用户真正的需求,这样不但会导致功能冗余,还会分散用户对于最重要功能的注意。事实上,根据图书标题排序这个需求很可能是想帮助用户了解某本书是否已经存在于列表中,或者某本书的具体信息,如还可以读多久,本质上,这个需求很可能是帮助用户从列表中快速查找某本书。如果是这样,为什么不考虑给出一个即时搜索的功能,比如说,当用户单击搜索按钮时,会显示一个搜索框,用户在里面输入关键字,图书列表马上显示包含该关键字的图书?

命令与操作

到目前为止,这个应用几乎可以说一无是处,因为它不支持添加、编辑和删除等操作,那么,实现这些操作又有哪些东西需要考虑呢?假设我们在用户界面上添加相应的按钮和菜单,如图 4 所示:

图 4

以往的做法是在代码隐藏文件里通过事件处理程序来实现,但在 MVVM 模式里,我们提倡通过命令对象来实现,问题是,这些命令对象在哪实现?添加操作是页面范围的,与之对应的命令对象可以在 MainViewModel 类里实现,但编辑和删除两个操作对应于 Book 类,那么,我们是否应该在 Book 类里添加相应的行为?

有一部分人对此的观点是,模型并非单纯的 POCO,而是完整的领域模型,可以包含区别于页面逻辑的业务逻辑,并且不会和 ORM 的实体类等同起来,这样做的好处是我们有一个统一的地方来维护整个应用的状态,也和具体的数据层解耦,无论数据最终来自本地还是远程服务,都不会影响在此之上的东西,与此同时,我们也不必在为不同的视图模型之间如何传递数据感到烦恼。当然,这样做的坏处也是明显的,它引入了大量可能不必要的复杂性,对于小型项目具有不少杀伤力。

如果我们不在 Book 类里添加行为,又不想在代码隐藏文件里通过事件处理程序来实现这些操作,那么我们就需要考虑一下 Expression Blend 的行为(Behavior)了。具体的想法是这样的,假设用户单击编辑菜单项的时候将会打开 EditItemPage.xaml 页面,而这个页面需要知道用户选中哪本图书,那么整个操作就可以看作通过 NavitagionService.Navigate 方法打开“EditItemPage.xaml?title=XXX”这样的链接了。要实现这样的效果,你可以使用 AppBarUtils for Windows Phone SDK 7.1 的 NavigateWithQueryStringAction,如代码 1 所示:

复制代码
<AppBarUtils:NavigateWithQueryStringAction TargetPage="/EditItemPage.xaml">
<AppBarUtils:Parameter Field="title" Value="{Binding Title}"/>
</AppBarUtils:NavigateWithQueryStringAction>

代码 1

配合 EventTrigger 在 MenuItem 上使用就可以实现预期的效果了。

除此之外,你也可以考虑创建一个 ItemViewModel 类,然后在上面实现编辑操作的命令对象,然后和 MenuItem 的 Command 属性进行绑定。如果你选择这种做法,就会无可避免地遇到在视图模型里打开页面的问题。我们通常用来打开页面的 NavitagionService.Navigate 方法必须在页面的范围内才可访问,但视图模型对于视图一无所知,怎么调用这个方法?

常见的做法是封装 PhoneApplicationFrame 的 Navigate 方法。当你用 Visual Studio 创建一个 Windows Phone 项目时,App 类里面会有一个 RootFrame 属性,你可以通过这个属性调用 PhoneApplicationFrame 的 Navigate 方法。事实上,PhoneApplicationFrame 和页面是共用同一个 NavitagionService 对象的。

删除操作是一种很特别的操作,它同时涉及到集合以及里面的元素,但在 XAML 里,MenuItem 只能从父元素继承对应的 Book 对象,却无从知晓包含该对象的集合,这为实现删除操作造成极大困扰。常见的解决办法是把 MainViewModel 作为一个静态属性放在 App 类里,这样你就可以轻易访问到包含 Book 对象的 Books 集合。从这个角度来看,如果我们一开始就把模型设计成领域模型,负责管理和维护领域对象的状态,那么现在就不必把某个视图模型硬塞到 App 类里了。

应用程序栏以及其他

在 Windows Phone 上使用 MVVM 模式必定会遇到的一个障碍就是应用程序栏,它的问题在于它不是 Silverlight 控件,而是系统组件,这意味着它无法像通常的 Silverlight 控件那样进行数据绑定。市面上有不少解决方案,其中之一就是前面提到的 AppBarUtils for Windows Phone SDK 7.1 ,有兴趣的可以看看 Allen Lee 写的《AppBarUtils 使用指南》

如果你把模型设计成领域模型,那么你一定要注意Windows Phone 的“深度链接”(deep link),这种情况会在你使用Toast 通知和次要磁贴(secondary tile),并在用户单击打开应用的某个页面时出现。由于用户仅对某个页面感兴趣,而且当用户按返回键时会直接退出应用而不是按照应用的常规逻辑返回上一页,因此构建整个领域模型会显得劳师动众、耗费资源。

有人认为,使用MVVM 模式的一大好处是为Expression Blend 用户带来便利,确实是这样,数据绑定和命令对象的应用使得Expression Blend 用户更易通过可视化操作使用开发人员的后台代码。如果你的视图模型也会给Expression Blend 用户使用,那么你必须考虑的一点就是在视图模型里提供设计时数据,尤其是你的逻辑包含访问本地数据库或者Web Service,因为在Expression Blend 的设计器里无法执行这些代码。

最后不得不提的是,MVVM 模式使得我们可以绕过用户界面对应用的功能进行测试,包括单元测试,如果你有兴趣,可以看看Chenkai 的《Windows phone 应用开发[9]- 单元测试》


感谢侯伯薇对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012 年 4 月 03 日 00:0015491

评论

发布
暂无评论
发现更多内容

全网都在跪求的阿里Java修炼开发技术笔记,终于开放下载了

你看起来很好吃

Java 编程 架构师 后端开发

架构师培训 -12 hadoop

刘敏

架构师训练营第 0 期第 12 周作业

无名氏

除了方文山,用TA你也能帮周杰伦写歌词了

华为云开发者社区

AI 数据 周杰伦 modelarts 歌词

图解图库JanusGraph系列-一文知晓“图数据“底层存储结构(JanusGraph data model)

洋仔聊编程

janusgraph 图数据库 存储结构 图解源码分析

云小课 | 一份超实用的勒索病毒自救预防指南

华为云开发者社区

勒索病毒 弱密码 云小课 企业主机安全 病毒云查杀

使用 Next.js , Nexus, Prisma 构建全栈项目

夏木

nextjs prisma graphql fullstack

阿里P8大牛力荐:Java程序员进阶必读的书籍清单(附电子版)

Java成神之路

Java redis 编程 程序员 JVM

架构师培训十二周练习

小蚂蚁

Github下载即将破百万的PDF:双十一高并发亿级流量秒杀顶级教程

你看起来很好吃

Java 编程 程序员 秒杀 计算机

不按套路出牌的阿里面试官:“刁难”面试者常用套路,你中招了吗

周老师

Java 编程 程序员 架构 面试

GitHub上120K Stars国内第一的Java多线程PDF到底有什么魅力?

你看起来很好吃

Java 程序员 并发编程 多线程 架构师

区块链USDT支付系统,USDT承兑支付软件开发

13530558032

LeetCode题解:155. 最小栈,使用链表代替栈,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

一文说透"静态代理"与"动态代理"

洋仔聊编程

源码分析 动态代理 静态代理

收藏!一篇教会你写90%的shell脚本!

洋仔聊编程

Shell shell脚本编写 收藏教程

京东T9今年首发的一份Spring Boot实战,让开发像搭积木一样简单

你看起来很好吃

Java 编程 程序员 架构师 计算机

我们从Kubernetes发展中学到了什么(1)

华宇法律科技

Kubernetes 容器 k8s

如何实现特定列脱敏?这两种方法你都要会

华为云开发者社区

postgresql 数据 脱敏 匿名 视图

易观郭炜:流动水系数造未来

易观大数据

膜拜!京东T9大牛沉淀三年终于整理出了这份架构核心修炼之道

你看起来很好吃

Java 编程 程序员 架构师 计算机

真香警告!手绘172张图解HTTP协议+703页TCP/IP协议笔记

你看起来很好吃

Java 程序员 架构师 计算机

TCP/IP协议族(第四版)已出,不愧是世界计算机优秀畅销精选书籍

你看起来很好吃

Java 编程 架构师 TCP/IP 协议族

2. Bean Validation声明式校验方法的参数、返回值

YourBatman

参数校验 Hibernate-Validator Bean Validation 方法校验

Spring+多线程+集合+MVC+数据结构算法+MyBatis源码学习笔记分享

Java成神之路

Java spring 编程 程序员 多线程

数字货币交易系统应用开发,区块链交易所app

13530558032

架构师训练营第十二周作业

叮叮董董

疫情对在线教育的影响

anyRTC开发者

在线教育 直播 RTC 安卓

GitHub上的今年第一本《Java异步编程实战》美团T9亲荐,太赞了

你看起来很好吃

Java 程序员 架构师 异步编程

Redis问的太深入,面试官说:你先回去等通知吧

你看起来很好吃

Java redis 编程 程序员 架构师

架构师训练营十二周作业

方堃

NLP领域的2020年大事记及2021展望

NLP领域的2020年大事记及2021展望

MVVM启示录-InfoQ