Model-View-ViewModel (MVVM) 设计模式描述了建立Windows Presentation Foundation 或 Microsoft Silverlight 应用程序的常用方法。Robert McCarter 演示了 ViewModel 的工作原理,并讨论了用您的代码实现 ViewModel 的优缺点。
PS: 本文来自MSDN,由Robert McCarter大神所写,具体地址已经未知,我只是将其排下版转载过来而已不过本文从各个方面详细地介绍了MVVM如何运作与实现.实在是一篇不可多得的好文
Windows Presentation Foundation (WPF) 和 Silverlight 提供了丰富的 API 用来构建现代应用程序,但是了解并和谐一致地应用所有 WPF 特性来构建设计精良、易于维护的应用程序可能非常困难。从何处入手?什么样的方法才算是正确的应用程序设计方法?
Model-View-ViewModel (MVVM) 设计模式描述了构建 WPF 或 Silverlight 应用程序的常用方法。它还是一款构建应用程序的强大工具,以及一种与开发人员讨论应用程序设计的通用语言。虽然 MVVM 确实很有用,但它发展时间不长,用户尚未形成正确的认识。
MVVM 设计模式什么时候是适用的,什么时候又是不必要的?应该如何设计应用程序的结构?ViewModel 层有多少代码要编写和维护,有什么替代方式能够减少 ViewModel 层的代码量?如何妥善处理 Model 中的相关属性?应该如何在 View 中显示 Model 中的集合?应该在哪里实例化 ViewModel 对象,并将其挂接到 Model 对象?
在本文中,我将解释 ViewModel 的工作原理,并讨论在您的代码中实现 ViewModel 的优缺点。我还会介绍一些具体的示例,演示如何使用 ViewModel 作为文档管理器,以便在 View 层中显示 Model 对象。
------------------------------------------------
Model、ViewModel 和 View
到目前为止,我设计过的每个 WPF 和 Silverlight 应用程序都具有相同的高层组件设计。Model 是应用程序的核心,需要投入大量精力,按照面向对象的分析和设计 (OOAD) 最佳做法进行设计。
对我来说,Model 是应用程序的核心,代表着最大、最重要的业务资产,因为它记录了所有复杂的业务实体、它们之间的关系以及它们的功能。
Model 之上是 ViewModel。ViewModel 的两个主要目标分别是:使 Model 能够轻松被 WPF/XAML View 使用;将 Model 从 View 分离并对 Model 进行封装。这些目标当然非常好,但是由于一些现实的原因,有时并不能达到这些目标。
您构建的 ViewModel 知道用户在高层上将如何与应用程序交互。但是,ViewModel 对 View 一无所知,这是 MVVM 设计模式的重要部分。这使得交互设计师和图形设计师能够在 ViewModel 的基础上创建优美、有效的 UI,同时与开发人员密切配合,设计适当的 ViewModel 来支持其工作。此外,View 与 ViewModel 的分离还使得 ViewModel 更有利于单元测试和重用。
为了在 Model、View 和 ViewModel 层之间实施严格的分离,我喜欢将每一层构建为一个单独的 Visual Studio 项目。与可重用的实用工具、主要的可执行程序集以及任何单元测试项目(您有大量这些内容,对吗?)结合之后,这会产生大量项目和程序集,如图 1 所示。
图 1 MVVM 应用程序的组成部分
由于这种严格分离的方法会产生大量项目,因此它显然最适合大型项目。对于只有一两位开发人员的小型应用程序来说,这种严格分离带来的好处可能无法抵消创建、配置和维护多个项目所带来的不便,因此仅仅将您的代码分离到同一个项目的不同命名空间中,可能比充分隔离更好用。
编写和维护 ViewModel 并不容易,不应轻率地对待。但是,一些最基本问题(MVVM 设计模式什么时候是适用的,什么时候又是不必要的)的答案经常包含在您的域模型中。
在大型项目中,域模型可能非常复杂,需要精心设计数以百计的类,使它们能够在任何类型的应用程序(包括 Web 服务、WPF 或 ASP.NET 应用程序)中顺畅地结合在一起。Model 可能由几个相互配合的程序集组成,甚至在超大型组织中,域模型有时是由一个专门的开发团队构建和维护的。
如果您有一个复杂的大型域模型,则引入 ViewModel 层几乎总是会带来好处。
另一方面,域模型有时很简单,可能仅仅是覆盖在数据库上的一个薄层。类可以自动生成,而且通常会实现 INotifyPropertyChanged。UI 通常是一系列可供编辑的列表或表格,允许用户对底层数据进行操作。Microsoft 工具集一直都极其擅长轻松快捷地构建这类应用程序。
如果您的模型或应用程序是这种类型的,则 ViewModel 很可能会带来难以接受的高开销,而对您的应用程序设计并没有足够的好处。
尽管如此,即使在这些情况下,ViewModel 也仍然有其价值。例如,ViewModel 非常适合用来实现“撤消”功能。另外,您也可以选择在应用程序的某个部分(例如文档管理,我将在后面讨论)使用 MVVM 直接向 View 提供 Model。
----------------------------------------------------------------------------------
为什么要使用 ViewModel?
即使 ViewModel 看起来适合您的应用程序,在开始编写代码之前,仍然还有问题需要解答。其中最重要的问题是如何减少代理属性的数量。
MVVM 设计模式将 View 从 Model 分离,这种做法是该模式的一个重要且有价值的方面。因此,如果 Model 类有 10 个属性需要在 View 中显示出来,则 ViewModel 最终通常会有 10 个等效的属性,这些属性只是代理了对底层模型实例的调用。这些代理属性在设置时通常会引发属性更改事件,通知 View 该属性已更改。
并非每个 Model 属性都要有 ViewModel 代理属性,但是每个需要在 View 中显示的 Model 属性通常都有一个代理属性。代理属性通常如下所示:
public string Description { get { return this.UnderlyingModelInstance.Description; } set { this.UnderlyingModelInstance.Description = value; this.RaisePropertyChangedEvent("Description"); } }
任何稍微复杂一点的应用程序都会有数十或上百的 Model 类,这些类都需要按这种方式,通过 ViewModel 向用户显示出来。这正是 MVVM 所提供的分离的本质。
编写这些代理属性很繁琐,因此很容易出错,尤其是在引发属性更改事件时需要一个字符串,该字符串必须与属性的名称相匹配(并且不会包含在任何自动代码重构中)。为了消除这些代理事件,常见的解决方法是直接从 ViewModel 包装器显示模型实例,然后让域模型实现 INotifyPropertyChanged 接口:
public class SomeViewModel { public SomeViewModel( DomainObject domainObject ) { Contract.Requires(domainObject!=null, "The domain object to wrap must not be null"); this.WrappedDomainObject = domainObject; } public DomainObject WrappedDomainObject { get; private set; } ...
因此,ViewModel 仍然可以提供视图所需的命令和更多属性,而无需重复 Model 属性或创建大量代理属性。这种方法当然有其吸引力,尤其是在 Model 类已经实现了 INotifyPropertyChanged 接口的情况下。让模型实现此接口并不一定是坏事,它甚至是 Microsoft .NET Framework 2.0 和 Windows 窗体应用程序中常见的做法。尽管它会使域模型变得很散乱,但确实对 ASP.NET 应用程序或域服务很有用。
借助这种方法,View 对 Model 有一定的依赖性,但是这仅仅是通过数据绑定实现的间接依赖,而不需要从 View 项目对 Model 项目进行项目引用。因此,纯粹从实用角度出发,此方法有时候很有用。
但是,此方法实际上违背了 MVVM 设计模式的精神,并且会降低您在将来引入新 ViewModel 功能(例如“撤消”功能)时的能力。我遇到过这种方法导致大量返工的情况。想象一下这种并不罕见的情况:深度嵌套的属性上有一个数据绑定。如果 Person ViewModel 是当前的数据上下文,且 Person 拥有 Address,则数据绑定可能如下所示:
{Binding WrappedDomainObject.Address.Country}
如果您还需要在 Address 对象上引入更多 ViewModel 功能,您就需要删除对 WrappedDomainObject.Address 的数据绑定引用,而改为使用新的 ViewModel 属性。这就会带来问题,因为对 XAML 数据绑定(可能还包括数据上下文)的更新很难进行测试。View 组件没有自动化的全面回归测试。
--------------------------------------------------------------
动态属性
我解决代理属性过多的方法是使用新的 .NET Framework 4 以及支持动态对象和动态方法调度的 WPF。后者使您能够在运行时决定如何处理类上并不存在的属性的读写操作。这意味着您可以消除 ViewModel 中的所有手写的代理属性,同时仍能封装底层模型。但是请注意,Silverlight 4 不支持绑定到动态属性。
实现此功能的最简单方法是让 ViewModel 基类扩展新的 System.Dynamic.DynamicObject 类,并重写 TryGetMember 和 TrySetMember 成员。当被引用的属性在类上不存在时,动态语言运行时 (DLR) 就会调用这两个方法,使该类能够在运行时决定如何实现缺少的属性。结合少量的反射之后,只需编写几行代码,ViewModel 类就能动态代理对底层模型实例的属性访问:
public override bool TryGetMember ( GetMemberBinder binder, out object result) { string propertyName = binder.Name; PropertyInfo property = this.WrappedDomainObject.GetType().GetProperty(propertyName); if( property==null || property.CanRead==false ) { result = null; return false; } result = property.GetValue(this.WrappedDomainObject, null); return true; }
该方法开始时使用反射来查找底层 Model 实例上的属性。如果该模型没有这样一个属性,该方法将失败并返回 False,数据绑定也失败。如果属性存在,该方法将使用属性信息来检索并返回 Model 的属性值。与传统代理属性的 get 方法相比,这是额外的工作,但这也是您需要为所有模型和所有属性编写的唯一实现。
动态代理属性方法的真正强大之处在于属性设置器。在 TrySetMember 中,您可以包含常见的逻辑,例如引发属性更改事件。其代码如下所示:
public override bool TrySetMember(SetMemberBinder binder, object value) { string propertyName = binder.Name; PropertyInfo property = this.WrappedDomainObject.GetType().GetProperty(propertyName); if( property==null || property.CanWrite==false ) return false; property.SetValue(this.WrappedDomainObject, value, null); this.RaisePropertyChanged(propertyName); return true; }
同样,该方法开始时使用反射从底层 Model 实例获取属性。如果属性不存在或是只读的,该方法将失败并返回 False。如果属性存在于域对象上,将使用属性信息来设置 Model 属性。然后您就可以包含对所有属性设置器均通用的逻辑。在此示例代码中,我只是为刚才设置的属性引发了属性更改事件,但您可以轻松完成更多任务。
待续.......