专注、自由、分享

Live your dream and share your passion

  :: :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
本文是一片译文,源于对作者文章内容和叙述方式的认可,自己从翻译的过程中也能再一次的学习该模式。
原文作者:Jeremy Likness [Microsoft Silverlight MVP]
 

内容介绍

本篇博文的目的是为了介绍模型-视图-视图模型(MVVM)设计模式,在我参加过得多次关于MVVM的线上讨论中,我经常听到该模式的初学者抱怨关于该模式的资源不够或是很多资源互相冲突,以至于在自己的代码中实现该模式非常的吃力,在此我不想介绍一些教条性的东西,只是想把主要概念汇聚到一篇博文中,使得大家能够很容易很直接的去理解该模式的价值以及如何去实现它。MVVM模式真的要远比人们所认为的简单。

 

背景知识

作为一个开发人员,你为什么要关心MVVM设计模式,对于WPF和Silverlight开发者来说,该模式能让你收益良多,在继续下文之前,请思考以下问题:

  • 你是否需要和设计师共享一个项目,是否需要设计和开发工作几乎同时进行的灵活性?
  • 在你的解决方案中,你是否需要完全的单元测试?
  • 在你的组织中,无论是项目内还是项目间,拥有可重用的组件对你来说是否重要?
  • 你是否需要改变界面而不用重构逻辑代码的灵活性?

如果对于任意一个问题你的答案都是YES,那么使用MVVM模式将能够给你的项目带来诸多益处。

 

对于我在线上看到的一些对话,我感到有点吃惊,比如有些想法是这样的:“MVVM只对极度复杂的UI有意义”,“对于小应用程序来说MVVM添加了太多的不必要的东西并且有点过于复杂了”。他们真正的牢骚就是:“MVVM不够灵活”。依我看,像这样的表述针对的是对MVVM模式的认知和实现,而不是MVVM模式本身,换句话说,如果你认为在实现MVVM模式时花费了太多的时间,那么说明你没有用正确的方式实现它,如果你的程序不能灵活扩展,不要责怪MVVM, 应该怪你使用MVVM的方式,我想无论你选择怎样的模式,把100,000条记录绑定到一个列表框中都是很愚蠢的做法。

 

所以,免责声明如下:这是我所知道的MVVM,并不是作为普遍真理的MVVM,我鼓励你使用评论的方式分享你的想法、经验、反馈和意见,如果你感觉有什么不正确的地方,请告诉我,我将尽我所能纠正并更新本文。

 

模型

模型,我喜欢把它叫做领域对象,模型代表我们正在处理的真实的数据和/或者信息。一个模型的示例可以是一个联系人(包含名称,电话号码,地址等等),或者是一个发布点现场直播数据流的流量特性。

 

要记住模型的关键是保存信息而不是那些操作信息的行为和服务,它没有为了让文本在屏幕上看的更漂亮而去格式化文本的责任,也不会从远端服务上去取回一列数据(事实上,在这列数据上,每一个条目同时也可能是一个它自身所拥有的模型),一般情况下,业务逻辑应该从模型中分离,并被封装到其他的一些操作模型的类中。但也不总是这样,比如,有些模型中可能会包含有验证规则。

 

保证一个模型完全的“清洁”是有点难度,在此,我指的是真实世界的真实再现。比如,一个联系人记录可能包含一个“最近修改时间”和一个“修改用户的标识”,和一个“唯一标识”(数据库或持久化信息)。在真实世界里,对一个联系人来说“最近修改时间”可能没有什么真正的意义,而只是一个模型在系统中怎样被使用、跟踪、持久化的功能而已。

 

下面是一个保存有联系人信息的示例模型:

 

Code

 

视图

视图是我们大部分人都很熟悉的,它是同最终用户真正发生交互的唯一对象。它是数据的表现形式,视图有一定的自由度使得数据表现的更漂亮,例如,一个日期数据可能被当做从1970年1月1日0时(Unix 时间)算起的秒数而存储在模型中,但是,对于最终用户来说,这个日期将被表示成用户所在时区的年月日格式。一个视图同样还拥有和它自身关联的一些行为,比如,接收用户输入,视图管理所有的输入(键盘按键,鼠标移动,触摸板手势等等),这些输入最终都操作到模型的属性上。

 

相比于完全被Controller或Presenter所控制的,不知道模型存在的消极视图,在MVVM模式中,视图是主动的,它包含行为、事件和数据绑定,数据绑定最终需要知道相关的模型和视图模型。同时这些事件和行为最终要映射到属性,方法调用和命令上,视图还要负责处理他自己的事件,这些事件并不会完全移交给视图模型。

关于视图,我们需要记住的是视图并不负责维护自己的状态,相反,它会同视图模型同步这些状态。

下面是一个XAML格式的视图示例:

 

Code

注意各种绑定是视图同视图模型的集成和同步点。

 

视图模型

视图模型是这三要素中关键的一环,因为它引入了 Presentation Separation,或者叫做“保持视图同模型分离的细微差别”的概念。模型并不知道用户的日期视图应该怎样表示,而是由视图模型负责把日期转换为显示格式,模型只保存数据,视图只保存格式化日期,控制器扮演这两者间联络点的角色,控制器获取视图的输入并放置到模型中,或者它同某个服务交互去获取模型,然后转化成属性值并放置到视图上。

 

视图模型还暴露方法、命令和其他一些帮助维护视图状态的信息,它按照视图的动作结果去操作模型,并触发视图自身的一些事件。

 

MVVM, 在幕后进化了相当长一段时间后,在微软公司的John Gossman 的博文中介绍给公众,该博文是关于Avalon(WPF的开发代号)的,标题是:Introduction to Model/View/ViewModel pattern for building WPF Apps 当人们都在评论中议论的时候,此文产生过相当轰动一时判断之争。

 

我还听过把MVVM描述成为WPF(后来还有Silverlight)特殊设计的表示模型的一个实现的说法。

 

许多模式示例中,经常聚焦于为视图定义而出现的XAML,为命令和属性而生的数据绑定。这些示例更加关注模型的实现细节而不是模型本身,那也是我为什么用不同的颜色来显示数据绑定:

mvvm.png

下面是一个视图模型的例子,我创建了一个BaseINPC的类(为了实现“INotifyPropertyChanged”接口),该类有一个方法使得触发属性值更改事件更加容易。

Code

该视图模型很明显是为处理联系人列表而设计的,它还暴露一个委托命令和一个表示该委托是否被允许执行的标志(从而为视图维护状态),一般情况下,标志是命令对象的一部分,但是该示例是在Silverlight 3 下构建的,Silverlight 3 并不原生支持命令绑定,我想展示一个简单不需要花哨框架的解决方法。(看一看这个视频展示了将Silverlight 3 的示例转换到 Silverlight 4 并使用原生命令代替是多么的容易)。这个视图模型对服务做了一个具体的引用。

 

对于更大型的程序,我更愿意做外部引用或者使用依赖注入框架。我们能够拥有像这样开始创建然后按需重构的灵活性真的是太美妙了,你不用使用任何一种框架就能充分利用该模式,正如你从示例中看到的一样。他立马能获取"Contacts”列表,这个列表中硬编码了我和其他一些有点名气的人的联系信息。当然,电话号码都是假的了。

 

让我们做一点更加特别的改变,然后看一看在示例程序中这将是如何被实现的,这里是一张MVVM建立后的透视图:

viewmodel.png

我们能从这张快照中收集到什么信息呢?

首先,IConfig 代表一个配置服务(在一个报纸阅读器中,他可能包含账户信息和一些被获取的订阅),IService 代表一些服务,在报纸阅读器程序中使用这个接口从RSS源中获取订阅。

 

视图和视图模型

  • 视图和视图模型通过数据绑定、方法调用、属性、事件和消息进行通信。
  • 视图模型不仅暴露模型,还有属性(比如状态信息,就像“is busy”指示符)和命令。
  • 视图处理它自己的UI事件,然后通过命令映射到视图模型。
  • 视图模型上的模型和属性从视图上通过双向绑定的方式被更新。

 

经常反映模式实现的两种机制是WPF中的触发器(特别是数据触发器)和Silverlight中的 Visual State Manager(VSM),这些机制通过绑定UI上的行为到相关的模型上从而帮助实现模式。在Silverlight中,VSM应该作为转换和动画协调的首选方案。学习更多有关VSM

 

视图模型和模型

在本场景中视图模型完全为模型负责,幸运的是,它并不孤单:

  • 视图模型可以为数据绑定直接暴露模型,或者相关的属性。
  • 为了获取和操作它暴露给视图的属性,视图模型能够包含服务,配置数据等接口。

 

先有鸡还是先有蛋呢?

你或许听说过关于视图优先或视图模型优先的讨论,通常,我相信很多开发者应该会同意一个视图应该只有一个视图模型,不需要将多个视图模型附加到一个视图上,如果你考虑关注点分离,这会很有意义,因为如果你有一个“联系人小窗口”在屏幕上并绑定到一个“联系人视图模型”,同时一个“公司小窗口”绑定到一个“公司视图模型“,这些应该是分离的视图,而不是一个有两个视图模型的单一视图。

 

一个视图可能有很多其他的视图组合而成,其中每一个视图都有它自己的视图模型,有需要的时候视图模型可以组合其他视图模型(我经常看到人们组合视图模型,事实上他们真正想要的是视图模型间的消息传递)。

 

一个视图应该有一个视图模型,一个单一的视图模型可能被多个视图使用(比如,想象一下一个有3个视图并都绑定到一个驱动着向导过程执行的视图上的向导程序)。

 

视图优先

视图优先最简单的意思就是视图是视图模型创建或发现的驱动者,在视图优先场景中,视图一般使用定位器模式,或者通过MEF,Unity或其他类似的依赖注入框架,绑定到一个作为资源存在的视图模型中,对于管理视图和视图模型这是一个非常通用的方法,下面是一些关于此主题的文章:

 

本文中我使用的示例是视图优先模式。视图被创建后,视图模型才被附加上,在应用程序对象中,它看起来就像是这样:

Code

在这个示例中,为了尽量保持简单,没有使用任何框架去连接接口和具体的实现。

 

视图模型优先

视图模型优先是另一种把框架连接在一起的方法,在这个场景中,视图模型负责创建视图并把自己绑定到视图上,你可以看一看Eisenerg 在MIX上讨论的以约定为基础的框架的一个示例:Build your own MVVM Framework.

 

这里要说的是做一件事情有很多种方式。

 

一个基本的MVVM框架

依我看,一个基本的MVVM框架真的仅仅只需要两件事情:

1、一个继承自DenpendencyObject类型并实现了INotifyPropertyChanged接口从而完全支持数据绑定的类。

2、一些命令支持。

第二个问题存在于Silverlight 3 中,因为在Silverlight 3 中ICommand接口是提供了,但是没有被实现。在Silverlight 4 中,命令是完全可用的。命令使得从视图到视图模型的事件绑定更加容易。这些都是更容易使用MVVM模式的实现细节。

 

记住,在Blend和免费得Blend SDK中对绑定和行为都有很好的支持。你可以看一下我的视频,MVVM with MEF in Silverlight, 从中你能看到实现MVVM模式是多么的容易,甚至都不需要一个已有的框架。文章MEF instead of Prism for Silverlight 3中展示了如何构建自自己的命令对象。

 

用这个例子,我创建了一个处理属性改变事件的基类:

Code


我同时还实现了一个命令,你还可以有更通用的命令类型去处理不同的情况,但是再次强调,为了表述方便,我简单地创建了一个特定于它所执行的方法的委托命令,我使用一个消息框来确认委托的正常工作。如果你需要一些像ChildWindow一样更加优雅的方式的话,你可以读一下我在后面米搜书的场景,更好地去理解如何在MVVM中作为一个服务集成一个对话框。

Code


当它完成的时候,这个特殊的命令使用一个委托去做回调,但是只允许一个订阅者,如果对于命令存在多个消费者(或是视图模型),那么就要使用多播委托或者是事件的方式来实现。

 

在Silverlight 4中,我可以简单使用Command标签把一个按钮绑定到命令上,本示例我是在Silverlight 3环境中创建的,Silverlight 3并不原生支持命令绑定,为了创建绑定,再次强调,针对于本项目为了表述方便,我做了一个简单的触发器来调用命令,所以我可以简单地在XAML中绑定它。

 

对于我在这儿提供的示例,你可以查看示例程序。它非常简单并且只包含一个服务和一个带模拟数据库的视图模型。两个视图绑定到相同的视图模型,你可以点击一个联系人去查看它的细节信息,你还可以删除一个联系人。

 

我经常受到一些抱怨博文示例太简单的抱怨,我认为不依赖于任何其他框架来展示一个完整的应用程序,这个示例已经是相当的复杂了,尽管它没有展示多个页面和各种类型的视图。其中的原因是你们所不知的,因为我是一个顾问和合伙人,我会不断地为客户构建面向业务的框架,但我没有许可去共享这些代码。同时我为博文构建一些小的示例,我没有时间去构建更大的工作模型,尽管我很想这样去做,但是因为时间的原因,真的很难实现。

你可以从下面的链接中下载该示例的源代码:

image Download sample - 40.3 KB

 

我认为学习模式最简单的方法就是通过观察一个完成应用程序的构建过程。我在MVVM with MEF in Silverlight视频中做了演示。在那个视频中,我构建了一些简单的视图模型并展示了一个基于用户的选择动态变换的视图,并使用MEF框架把所有相关项联系到一起。更复杂的场景将在第二部分中介绍。

 

所以关于那些复杂的面向业务的,有不止一个按钮,更多视图,更复杂逻辑的解决方案,在细节上已经超出了本文所涉及的范畴。但是我还是很愿意去处理一些常见的场景并告诉大家我是如何使用MVVM解决这些问题的。

 

以我的经验,绑定命令和模型或属性非常的直接。当你遇到一些特殊的情形比如显示一个对话框或是触发一段动画,MVVM可能看起来有点令人迷惑。我们应该如何处理这些常见的问题呢?

 

列表选择

你如何用MVVM来处理一个用来单选或多选的组合框呢?实际上相当的简单,假设一个场景,该场景中你有一个联系人列表的组合框,当组合框列表被选择时,在同一个页面中有另一个视图显示联系人的详细信息,那么视图模型应该像下面这样,假设我使用MEF框架来处理依赖。

Code


在这种情况下,我们引入一个获取联系人信息的服务,连接到列表上,并设置当前联系人,下拉列表绑定到 Contacts 集合上,最重要的是选中条目总是被绑定,绑定应该像下面这样:

Code

这就确保了无论在列表中选择了哪一个,当前联系人都将被更新,记住我提到的多视图可以共享相同的视图模型。在这个例子中,联系人详细信息视图可以使用相同的视图模型,并简单绑定到CurrentContact属性。

 

导航

导航是一个要处理的常见问题,你如何从一个MVVM应用程序中管理导航呢?许多示例展示的仅仅是一个单一的按钮或窗口并没有处理有多个页面的组合应用程序。

最简单的答案就是无论你如何导航,你都应该抽象出一个接口后面的实现机制。通过定义INavigation或其他类似的接口,导航不再是一个MVVM问题了,无论你是如何解决的,你的视图模型都能引入INavigation接口并按需简单导航或触发转换。

我的文章MEF instead of Prism 展示了如何使用托管扩展框架来解决这个问题。Auto-discoverable views using a Fluent interface 文章中介绍了如何映射视图到区域。 Dynamic module loading with Prism 有一个完整的使用导航框架的解决方案。

 

动态加载模块

这个问题是紧跟着导航来的,如果你有一个非常大得应用程序会怎么样呢?立刻加载所有的资源通常没有意义,你只想主菜单出现在屏幕上,然后按照他们需要的去动态加载其他的模块。这样做减少了应用程序的启动时间,同时还涉及到用户浏览器或桌面应用程序的内存和CPU。

 

动态加载模块的问题并不是针对于MVVM的,但是视图模型间和跨模块间的消息交换非常的重要,为此,我相信学习已有的像MEF和Prism这些解决特定问题的框架非常的有意义,Prism能确保模块按需加载,MEF提供了一个允许动态加载XAP文件的部署目录。我将在附录中探讨更多有关解决类似问题的框架和解决方案。

 

对话框

一个常见的UI模式就是对话框(类似于一个消息框,但是期望获得一个响应)。我看到很多人被如何用MVVM和Silverlight的限制来实现所绊倒,Silverlight中要求所有的代码都是同步的。

最早的解决方法是将对话框抽象出一个接口并为响应提供一个回调。视图模型导入对话框,然后给予状态改变或是命令来触发对话框服务。回调将返回响应信息,然后视图就能相应地处理。

阅读更完整的解释,请浏览:Simple Dialog Service in Silverlight.

 

动画

有一个常见的问题:改变如何既能在UI中又能在后台触发动画和其他的转换。

有一些针对此问题的解决方法,下面是一下如何解决该问题的示例:

 

配置和全局值类型

另一个经常被提的问题是如何处理全局变量和配置信息。再一次重申,这不是一个MVVM问题,而是更普通的体系结构考虑。在大多数情况下,你可以用一个接口(IConfiguration)暴露配置信息,然后把一个具体的实现同你的配置信息连接起来,任何请求配置信息的视图模型只要简单导入这个实现,无论是通过MEF,Unity还是其他的机制,并且只有一个此类的拷贝(单态模式,通常是由容器而不是类本身来管理)。

 

异步处理

使用Silverlight汇总的一个容易混淆的地方是它强制服务调用都要是异步的,当创建一个视图模型的时候这看起来有点奇怪:当你出发了一个调用的是后,你怎么怎都它完成了呢?通常,

 

超大数据集

 

MVVM不是什么?

总结

附录 A 一些历史模式

附录 B 已经存在的MVVM框架

posted on 2011-09-20 11:36  我是柯南  阅读(807)  评论(0编辑  收藏  举报