基于MVVM设计模式的WPF应用程序.docx
《基于MVVM设计模式的WPF应用程序.docx》由会员分享,可在线阅读,更多相关《基于MVVM设计模式的WPF应用程序.docx(31页珍藏版)》请在冰点文库上搜索。
基于MVVM设计模式的WPF应用程序
原创翻译:
基于MVVM设计模式的WPF应用程序
Postedon2011-07-0400:
11StarSoul阅读(43)评论(0)编辑收藏
第一次翻译英文技术文档,翻译不正确的地方请多多指正。
原文及文中演示程序的地址:
开发一个专业的应用程序软件的用户界面不容易。
它可能涉及到数据融合,交互设计,可视化设计,连接,多线程,安全,国际化,验证,单元测试和可触摸技术。
考虑到用户界面暴露了底层系统,必须满足其用户的不可预知的要求,它可以是许多应用中最不稳定的区域。
有流行的设计模式,可以帮助驯服这个难使用的野兽,但是正确的区分和选择多个关注可能是困难的。
模式越复杂,越有可能捷径在以后被使用,破坏了以前正确方式做事情的努力。
这不总是设计模式的问题。
有时我们用复杂的设计模式,需要写很多的代码因为使用的UI平台不能帮助它用一个简单的模式。
幸运地是,WPF提供了这一帮助。
由于软件界采用WPF的比率不断增长,WPF团体一直在开发它自己的模式生态系统和实践。
在这篇文章中,我将查阅那些用WPF设计和实现的最好实践中的一些。
通过利用一些结合MVVM设计模式的WPF的核心特征,我将介绍一个实例程序,演示以”正确方式”构建一个WPF程序是多么简单。
在这篇文章的结尾,我们将清楚数据模板,命令,数据绑定,资源系统和MVVM模式如何结合在一起,创建一个简单地,可测试的,强大的框架,在此框架上任何WPF程序都能存活。
伴随着这篇文章的演示程序能作为一个真正的WPF程序的模板,用MVVM作为它的核心架构。
在演示解决方案的单元测试显示了如何轻松地测试应用程序的用户界面功能,该功能在一系列的ViewModel类中存在。
在进入细节之前,让我们首先回顾一下为什么要用MVVM这样的模式。
有序vs混乱
在一个简单的”Hello,World!
”程序里使用设计模式是没有必要的,只会适得其反。
任何一个合格的开发人员能一目了然的理解几行代码。
然而,当程序的功能增加时,代码行和移动部件的数量相应地也会增加。
最终,系统的复杂性,以及它包含的重复出现的问题,促使开发人员去重构他们的代码,以便它们更容易理解,讨论,扩展和解决问题。
我们通过在源代码中对于某些实体应用众所周知的名称来减少认知上的混乱。
我们通过考虑它在系统中的功能角色,确定一段代码中的名称。
开发者经常根据一个设计模式刻意地构建他们的代码,而不是让模式自然地出现。
这两种方法都没有错,但是在这篇文章里,我考察了在一个WPF应用程序中明确地使用MVVM作为架构的好处。
某些类的名称包含了MVVM模式中熟悉的术语,例如如果这个类是一个view的抽象,类名称以”ViewModel”结尾。
这种方法有助于避免前面提到的认知混乱。
你能很愉快地存在于一个混沌的控制,这是在最专业的软件开发工程中事务最自然的状态。
Model-View-ViewModel的革命
从人们开始创建软件用户界面开始,已经有流行的设计模式帮助使它更容易。
例如,MVP模式已经在各种UI编程平台享有声望。
MVP是Model-View-Controller模式的变种,这十多年来一直使用很广泛。
万一你以前从来没有用过MVP模式,这里有一个简单的解释。
你在屏幕上看到的是View,它所显示的数据是Model,而Presenter把这两者连接在一起。
View依赖Presenter去组装数据,和用户输入进行交互,提供输入验证(可能通过委托的模型),以及一些其他的任务。
如果你想要学习更多的关于MVP的东西,我建议你阅读一下Jean-PaulBoodhoo's2006年8月份的设计模式专栏。
早在2004年,MartinFowler发表了一篇命名为PresentationModel(PM)的模式的文章。
PM模式相似于MVP,它分离出来view的行为和状态。
PM模式的有趣的部分在于一个view的抽象被创建,叫做PresentationModel。
View仅仅变成一个PresentationModel的一个表现。
在Fowler的解释里,他显示了PresentationModel频繁地更新它的View,以便这两者之间保持一致。
同步的逻辑作为代码存在于PresnetationModel类里。
在2005年,JohnGossman,当前微软WPF和Silverlight的架构师之一,在他的blog里发表了Model-View-ViewModel(MVVM)模式。
MVVM与Fowler的PresentationModel相同,这两个模式都有一个View的抽象,其中包含了View的状态和行为。
Fowler引入PresentationModel作为一种与UI平台无关的View的抽象的创建,而Gossman引入MVVM作为一种利用WPF的核心特征去简化用户界面的创建的标准化的方式。
在这个意义上,我考虑MVVM是PM模式的更一般化,是为WPF和Silverlight平台而特制的。
在GlennBlock2008年9月发表的一篇优秀的文章”Prism:
PatternsforBuildingCompositeApplicationwithWPF”,他解释了微软WPF的组合应用程序指导。
术语ViewModel没有被使用,术语PresentationModel被使用去描述View的抽象。
贯穿于这篇文章,我将这个模式作为MVVM,view的抽象作为ViewModel。
我发现这个术语是在WPF和Silverlight团体里是更流行的。
不同于MVP里的Presenter,ViewModel不需要一个view的引用。
View绑定到ViewModel的属性上,这反过来,公开了在model对象里包含的数据和其他的指定到view上一些其他状态。
在View和ViewModel之间的绑定被很简单地构建,因为一个ViewModel对象被设置作为一个view的DataContext。
如果在ViewModel里的属性值改变,那些新的值自动地通过数据绑定传播到view。
当用户点击View上的一个button时,ViewModel上的一个command去执行一个请求的行为。
ViewModel,而不是View,执行所有模型数据的修改。
View类不知道model类的存在,而ViewModel和Model都不知道view。
实际上,Model完全地遗忘了ViewModel和View存在的事实。
这是非常松耦合的设计,你很快就能看到这在很多方面的好处。
为什么WPF开发人员喜爱MVVM
一旦开发人员变得与WPF和MVVM舒适,它是很难去区分这两个。
MVVM是WPF开发人员的专用语言,因为它很适合WPF平台,WPF被设计使它很容易使用MVVM模式去构建应用程序。
实际上,微软内部也用MVVM去开发WPF应用程序,如MicrosoftExpressionBlend,而核心WPF平台正在建设中。
WPF的很多方面,例如look-less控件模型和数据模板,利用强壮的从状态和行为中分离显示。
WPF使得MVVM成为一个强大的模式的一个最重要的方面是数据绑定。
通过一个view到Viewmodel的绑定属性,你在两者之间得到松耦合,完整地移除了在ViewModel里直接写代码更新一个view的需要。
数据绑定系统也支持输入验证,提供了传递验证错误到view的标准方式。
WPF使得这个模式如此有用的其他两个特征是数据模板和资源系统。
数据模板应用View到ViewModel对象显示在用户界面。
你能在XAML里声明模板,让资源系统为你在运行时自动地加载和应用这些模板。
你能学习更多的关于绑定和数据模板的知识在2008年7月的文章里,“DataandWPF:
CustomizeDataDisplaywithDatabindingandWPF”。
如果WPF不支持commands,MVVM模式不会如此强大。
在这篇文章里,我将演示一个ViewModel怎样公开commands到一个View,允许View去消费它的功能。
如果你不熟悉命令,我推荐你阅读BrianNoye2008年9月发行的一篇综合性的文章“AdvancedWPF:
UnderstandingRoutedEventsandCommandsinWPF”,除了WPF(以及Silverlight2)的特征使得MVVM自然地方式去构建应用程序,这个模式也似流行的因为ViewModel类容易做单元测试。
当一个应用程序的交互逻辑位于一系列的ViewModel类时,你能很容易地写代码去测试它。
在这个意义上,View和单元测试仅仅是两个不同类型的ViewModel的消费者。
有一套测试程序为应用程序的ViewModel提供自由的测试,帮助减少维护应用程序的消耗。
为了推广自动化回归测试的创建,ViewModel类得易测性可以帮助正确地设计容易换肤的用户界面。
当你正在设计一个应用程序时,你能经常决定一些东西是应该在view里还是在viewmodel里,通过假想你想要写一个单元测试去消耗ViewModel。
如果你能为Viewmodel写单元测试而没有创建任何UI对象,你也能完全地剥离ViewModel因为它不依赖于指定的可见元素。
最终,对于视觉设计人员的开发者,使用MVVM使得创建一个平滑的设计者/开发者工作流很简单。
由于一个view仅仅是一个ViewModel的任意的消费者,很容易把viw剥离出来,换上一个新的view去。
这个简单的步骤可以快速地开发原型和设计师开发出来的用户界面的评估。
开发团队能集中精力在ViewModel类,设计团队能集中精力制作用户友好的视图。
连接这两个团队的输出除了确保正确的绑定在视图的XAML文件中存在之外可能涉及更多。
演示程序
在这点,我已经查阅了MVVM的历史和操作的理论,我也检查了为什么它在WPF开发者中这么流行。
现在我们来看一看这个模式如何运转的。
伴随着这篇文章的演示程序用多种方式来使用MVVM。
它提供了丰富的例子源代码,把概念放入了上下文中。
我创建这个演示程序在VisualStudio2008SP1,.NET框架3.5SP1。
单元测试运行在VisualStudio单元测试系统。
程序能包含任意数量的”工作空间”,每个工作空间用户能通过点击左边导航区域的命令链接来打开。
所有工作空间存在于主要内容区域的TabControl。
用户能点击工作空间tab项上的关闭按钮来关闭工作空间。
应用程序有两个可用的工作空间:
”AllCustomers”和”NewCustomer”。
在运行程序打开工作空间后,界面显示如Figure1:
Figure1工作空间
一次仅仅能打开”AllCustomers”工作空间的一个实例,但是任意数量的”NewCustomer”工作空间能被打开。
当用户决定去创建一个新的客户时,她必须填写Figure2的数据输入表格。
Figure2 新客户数据输入表格
在用有效的值填写了数据输入表格并点击保存按钮后,新客户的名字出现在tab页,并且那个客户被加入到所有客户的列表中。
这个程序不支持删除或者编辑一个已存在的客户,但是那个功能和很多其他相似的功能,都可以在已存在的应用程序架构的基础上很容易实现。
现在你已经大体了解了这个演示程序所做的事情,接下来让我们调查它是如何设计和实现的。
RelayingCommand逻辑
程序每一个view都有一个空的后台文件,除了模板生成的代码(在类的构造函数中调用InitializeComponet)之外,实际上,你可以从工程中移除掉view的背后文件,应用程序将仍然能正确地编译和运行。
尽管视图里没有事件处理方法,当用户点击按钮时,应用程序仍然能反应和满足用户的要求。
这是因为绑定建立在UI上显示的超链接,按钮和菜单项控件上的Command属性。
那些绑定确保当用户点击控件时,ViewModel公开的ICommand对象执行。
你能认为command对象作为一个适配器,使得它很容易地从XAML中声明的视图中消耗ViewModel的功能。
当一个ViewModel公开一个ICommand类型的实例时,Command对象用ViewModel对象去得到它完成的工作。
一个可能的实现模式是在ViewModel类中创建一个嵌套的类,以便command能访问它包含的ViewModel对象的私有成员,不污染命名空间。
那些嵌套的类实现了ICommand接口,一个包含ViewModel对象的应用被插入到构造函数中。
然而,为每一个ViewModel公开的command创建一个实现ICommand的嵌套类将使得ViewModel类的尺寸膨胀。
更多的代码意味着更多潜在的bug。
在演示程序里,RelayCommand类解决了这个问题。
RelayCommand允许你去注入命令的逻辑通过在它的构造函数中声明。
这种方法允许在ViewModel类中简洁的,简明的命令实现。
RelayCommand是微软组合应用库中的DelegateCommand的一个简单地变种。
RelayCommand类如Figure3:
Figure3TheRelayCommandClass
publicclassRelayCommand:
ICommand
{
#regionFields
readonlyAction
readonlyPredicate
#endregion//Fields
#regionConstructors
publicRelayCommand(Action
:
this(execute,null)
{
}
publicRelayCommand(Action
{
if(execute==null)
thrownewArgumentNullException("execute");
_execute=execute;
_canExecute=canExecute;
}
#endregion//Constructors
#regionICommandMembers
[DebuggerStepThrough]
publicboolCanExecute(objectparameter)
{
return_canExecute==null?
true:
_canExecute(parameter);
}
publiceventEventHandlerCanExecuteChanged
{
add{CommandManager.RequerySuggested+=value;}
remove{CommandManager.RequerySuggested-=value;}
}
publicvoidExecute(objectparameter)
{
_execute(parameter);
}
#endregion//ICommandMembers
}
CanExecuteChanged事件,是ICommand接口实现的一部分,有一些有趣的特征。
它声明了到CommandManager.RequestSuggested事件的事件认购。
这确保WPF能指挥所有的RelayCommand对象如果他们能随时执行它要求的内置命令。
下面的CusteomerViewModel类的代码,显示了怎样用lambda表达式配置RelayCommand,我将在之后更进一步解释。
ViewCode
RelayCommand_saveCommand;
publicICommandSaveCommand
{
get
{
if(_saveCommand==null)
{
_saveCommand=newRelayCommand(param=>this.Save(),
param=>this.CanSave);
}
return_saveCommand;
}
}
ViewModel类继承关系
大部分的ViewModel类需要相同的特征。
他们经常需要实现INotifyPropertyChanged接口,他们经常需要有一个用户友好地显示名称,在工作区间的例子里,他们需要能力去选择是否能被从UI上移除。
这个问题自然地使得需要创建一个ViewModel的基类,以便ViewModel类能从基类里继承所有恶共通的功能。
ViewModel类的继承层次表如Figure4所示:
Figure4 InheritanceHierarchy
拥有一个所有ViewModel类的基类绝不是必须的。
如果你愿意通过组合使很多细小的类在一起,而不是使用继承,那不是一个问题。
正如任何一个其他的设计模式,MVVM是一系列的指导,不是规则。
ViewModelBase类
ViewModelBase是继承关系上的根类,这就是为什么它实现共通INotifyPropertyChanged接口并且有一个DisplayName属性。
INotifyPropertyChanged接口包含一个叫做PropertyChanged的事件。
当任何时候一个ViewModel对象的一个属性有一个新的值,它能引起PropertyChanged事件去通知WPF绑定系统这个新值。
一旦收到通知,绑定系统查询属性,一些UI元素的绑定属性也收到新的值。
为了让WPF知道ViewModel对象的哪一个属性发生改变,PropertyChangedEventArgs类公开一个字符串类型的PropertyName属性。
你必须小心地传递正确的属性名字到事件参数中,否则,WPF将终止为新值查询错误的属性。
ViewModelBase的一个有趣的方面是通过一个在ViewModel对象实际存在的给定名称提供验证属性的能力。
这是非常有用的重构,因为通过VisualStudio2008重构特征改变一个属性名将不能更新你源代码中的碰巧包含那个属性名的字符串(也不应该)。
在事件参数中用不正确的属性名引起PropertyChanged事件能导致很难追查的细微的错误。
Figure5VerifyingaProperty
//InViewModelBase.cs
publiceventPropertyChangedEventHandlerPropertyChanged;
protectedvirtualvoidOnPropertyChanged(stringpropertyName)
{
this.VerifyPropertyName(propertyName);
PropertyChangedEventHandlerhandler=this.PropertyChanged;
if(handler!
=null)
{
vare=newPropertyChangedEventArgs(propertyName);
handler(this,e);
}
}
[Conditional("DEBUG")]
[DebuggerStepThrough]
publicvoidVerifyPropertyName(stringpropertyName)
{
//Verifythatthepropertynamematchesareal,
//public,instancepropertyonthisobject.
if(TypeDescriptor.GetProperties(this)[propertyName]==null)
{
stringmsg="Invalidpropertyname:
"+propertyName;
if(this.ThrowOnInvalidPropertyName)
thrownewException(msg);
else
Debug.Fail(msg);
}
}
CommandViewModel类
最简单的具体的ViewModelBase子类是CommandViewModel。
它公开一个ICommand类型的叫做Command的属性。
MainWindowViewModel通过它的Commands属性公开这些对象的集合。
在主窗口左边的导航区域显示每一个MainWindowViewModel公开的CommandViewModel的链接,例如“Viewallcustomers”和“Createnewcustomer”。
当用户在链接上点击时,执行那些命令中的一个,一个工作空间在主窗口的TabControl上打开。
CommandViewModel类定义显示在这里:
ViewCode
publicclassCommandViewModel:
ViewModelBase
{
publicCommandViewModel(stringdisplayName,ICommandcommand)
{
if(command==null)
thrownewArgumentNullException("command");
base.DisplayName=displayName;
this.Command=command;
}
publicICommandCommand{get;privateset;}
}
在MainWindowResource.xaml文件存在一个关键字是“CommandsTemplate”的数据模板。
MainWindow用模板去映射前面提到的CommandViewModel的集合。
模板简单地映射每一个CommandViewModel对象作为一个ItemsControl的链接。
每个超链接的Command属性被绑定到一个CommandViewModel的Command属性。
那个XAML被显示在Figure6:
Figure6RendertheListofCommands
--InMainWindowResources.xaml-->
--
Thistemplateexplainshowtorenderthelistofcommandson
theleftsideinthemainwindow(the'ControlPanel'area).
-->
Key="CommandsTemplate">