MVVM模式构建WPF.docx
《MVVM模式构建WPF.docx》由会员分享,可在线阅读,更多相关《MVVM模式构建WPF.docx(33页珍藏版)》请在冰豆网上搜索。
MVVM模式构建WPF
使用MVVM设计模式构建WPF应用程序
本文是翻译大牛JoshSmith的文章,WPFAppsWithTheModel-View-ViewModelDesignPattern,译者水平有限,如有什么问题请看原文,或者与译者讨论(非常乐意与你讨论)。
本文讨论的内容:
WPF与设计模式
MVP模式
对WPF来说为什么MVVM是更好的选择
用MVVM构建WPF程序
本文涉及的技术:
WPF、数据绑定
内容列表
有序与混乱
模型-视图-视图模型的演变
为什么WPF开发者喜欢MVVM
演示程序
中继命令逻辑
ViewModel类层级结构
ViewModelBase类
CommandViewModel类
MainWindowViewModel类
View对应ViewModel
数据模型和Repository
新增客户数据表单
所有客户视图
总结
开发UI,对一个专业软件并不容易。
它需要未知数据、交互式设计,可视化设计、联通性,多线程、国际化、验证、单元测试以及其他的一些东西才能完成。
考虑到UI要展示开发的系统并且必须满足用户对系统风格不可预知的变更,因此它是很多应用程序最脆弱的地方。
有很多的设计模式可以帮助解决UI不断变更这头难缠的野兽,但是恰当的分离和描述多个关注点可能很困难。
模式越复杂,之后用到的捷径越可能破坏之前正确的努力。
这并不总是设计模式的错。
有时使用要写很多的代码复杂设计模式,这是因为我们使用的UI平台并不适合简单是设计模式。
UI平台需要做的是很容易使用简单的,久经考验的,开发者认识的设计模式构建UI。
庆幸的是,WPF就是这样一个平台。
随着是使用WPF开发的比例不断升高,WPF社区发展了自己的模式与实践生态圈子。
在本文,我将讨论一些设计与实现客户端应用程序的WPF最佳实践。
利用WPF和MVVM设计模式衔接的一些核心功能,我将通过一个例子介绍,用“正确”的方式构建一个WPF程序是多么的简单。
datatemplates,commands,databinding,theresourcesystem以及MVVM模式怎么揉合到一起创建一个简单的、可测试的、健壮的框架,并且任何WPF程序都能使用,到文章最后,这一切都很清晰明了。
文中的例程可以作为现实中一个WPF应用程序的模版,并且使用MVVM设计模式作为其核心架构。
例程解决方案中的单元测试部分,展示了测试ViewModel类的功能是很容易的。
在深入本文之前,我们首先看一下我们要使用像MVVM这样的设计模式。
有序与混乱
没有必要在一个”Hello,World!
”的程序中使用设计模式。
任何一个合格的开发者看一眼就指导那几行代码是干什么的。
然而随着程序功能点的增加,随之代码的数量以及移动部件也会增多。
最终系统的复杂度以及不断出现问题,促使开发者组织他们的代码,以便它们更容易理解,讨论、扩展以及维护。
我们通过给代码中某些实体命以众所周知的名字,减少复杂系统认知误区。
我们给函数块命名主要依据系统中的功能角色。
开发者有意识的根据设计模式组织他们的代码,而不是根据设计模式自动去组织。
无论哪一种,都没有什么问题。
但是在本文中,我说明在WPF程序中明确使用MVVM模式的好处。
某些类的名称,包括MVVM模式中著名的术语,如果类是view的抽象类就以ViewModel结束。
这种方式有助于避免之前提到的认知误区。
相反,你也可以让那种受控的误区存在,这正是大部分软件开发项目的自热状态。
模型-视图-视图模型的演变
自从人们开始构建UI时,就有很多流行的设计模式让UI构建更容易。
比如,MVP模式在各种UI编程平台中都非常流行。
MVP是MVC模式的一种变体,MVC模式已经流行了几十年了。
以防你之前从没用过MVP模式,这里做一个简单的解释。
你在屏幕上看到的是View,它显示的数据是Model,Presenter就是把两者联系起来。
View依赖Presenter并通过Presenter展示Model数据,响应用户输入,提供数据验证(或许委托给Model去完成)以及其他的一些任务。
如果你想了解更过关于MVP模式,我建议你去读Jean-PaulBoodhoo的August2006DesignPatternscolumn。
2004年晚些时候,MartinFowler发表了一篇叫PresentationModel(PM)的模式。
PM模式和MVP类似,MVP是把一个View从行为和状态分离出来。
PM中令人关注的部分是创建view的抽象,叫做PresentationModel。
之后,View就仅仅是PresentationModel的展示了。
在Fowler的论文中,他展示了PresentationModel经常更新View,以便两个彼此同步。
同步逻辑组作为代码存在于PresentationModel类中。
2005年,JohnGossman,目前是微软WPF和Silverlight架构师,在他的博客上披露了Model-View-ViewModel(MVVM)模式。
MVVM和Fowler的PresentationModel是一致的,两个模式的特征都是View的抽象,都包含了View的行为和状态。
Fowler引入PresentationModel是作为创建独立平台的View的抽象,而Gossman引入MVVM是作为标准化的方法,利用WPF的核心特点去简化UI的创建。
从这种意义上来讲,我把MVVM作为一般PM模式的一个特例。
在GlennBlock一遍优秀的文章"Prism:
PatternsforBuildingCompositeApplicationswithWPF",于2008年9月微软大会发布,他解释了WPF微软组合程序开发向导。
术语ViewModel没有用到,然而PM却用来描述View的抽象。
这篇文章自始至终,都没没有出现我要将MVVM模式,以及View的抽象ViewModel。
我发现这个术语在WPF和Silverlight社区中比较流行。
不像MVP中的Presenter,ViewModel不需要引用View。
View绑定ViewModel的属性,ViewMode向Viewl暴露Model对象的数据以及其他的状态。
View和ViewModel之间的绑定很容易构造,因为ViewModel对象可以设置为View的DataContext。
如果ViewModel中的属性值发生改变,新值将通过绑定自动传送给View。
当用户点击View中的按钮时,ViewMode对于的Command将执行请求的动作。
ViewModel,绝不是View,去执行实体对象的修改。
View类并不知道Model类是否存在,同时ViewModel和Model也不知道View。
实际上,,Model完全不知道ViewModel和View存在,这是一个非常松耦合的设计,在很多方面都有好处,这不就你就会看到。
为什么WPF开发者喜欢MVVM
一旦开发者适应了WPF和MVVM,就很难区别两者。
因为MVVM非常适合WPF平台,并且WPF被设计使用MVVM模式更容易构建应用程序,MVVM就成了WPF开发者的通用语。
事实上,微软内部正在用MVVM开发WPF应用程序,像MicrosoftExpressionBlend,然而当时WPF平台的核心功能依然在开发之中。
WPF的很多方面,像控制模型以及数据模版,都利用了MVVM推荐的显示状态和行为分离技术。
MVVM之所以成为一个伟大设计模式,是因为WPF的一个最重要的特征数据绑定构造。
通过把Viewde属性绑定到ViewModel,你就可以得到两者松耦合的设计,并且完全去除ViewModel更新View的那部分代码。
数据绑定系统支持输入验证,并且输入验证提供了传递错误给View的标准方法。
另两个WPF的特点,数据模版和资源系统让MVVM模式更加可用。
数据模版把View应用在ViewModel对象上,以便其能够在UI上显示。
你可以在Xaml中声明模版,让资源系统在系统运行过程中自动定位并应用这些模版。
你可以从我2008年7月写的一篇文章,"DataandWPF:
CustomizeDataDisplaywithDataBindingandWPF.",获取更多关于绑定和数据模版的信息。
要不是WPF对Command的支持,MVVM模式就不会那么强大。
本文中,我会为你展示ViewModel怎样把Commands暴露给View,并且让View消费它的功能。
如果你对Command不是很熟悉,我推荐你读一下2008年9月BrianNoyes发布的文章,"AdvancedWPF:
UnderstandingRoutedEventsandCommandsinWPF"。
除了WPF(Silverlight2)本身让MVVM以一种自然的方式去构建程序之外,造成MVVM模式流行还有一个原因,那就是ViewModel类很容易进行单元测试。
从某种意义来讲,View和单元测试只是ViewModel两个不同类型的消费者。
拥有一套应用程序的单元测试,可以为提供更自由、快速的回归测试,而回归测试有助于降低之后应用的维护成本。
除了促进创建自动化回归测试外,ViewModel类的可测试性也有助于设计更容易分离的UI。
当你设计应用时,你可以通过想象某些东西是否要创建单元测试消费ViewModel,来确定它们是放到View里面还是ViewModel里面。
如果你可以为ViewModel写单元测试而不用创建任何UI控件,你也可以把ViewModel剥离出来,因为它不依赖任何具体可视化的组件。
最后,对于要和设计者合作的开发者来说,使用MVVM模式使得创建平滑的开发/设计工作流更加容易。
既然View可以是ViewModel的任意一个消费者,就很容易去掉一个View通过新增一个View去渲染ViewModel。
这个简单的步骤允许设计师构建快速原型以及评估UI设计。
这样开发团队可以关注创建健壮的ViewModel类,而设计团队可以关注设计界面友好的View。
要融合两个团队输出只需要在View的xaml上进行正确的绑定即可。
演示程序
到此为止,我们回顾了MVVM的历史以及具体操作理论。
我也说明了它在WPF开发者中间如此流行的原因。
现在是时候继续我们的步伐,看一下MVVM模式在实际中的应用。
这篇文章中的演示程序以各种方式使用MVVM设计模式,它提供了丰富的例子,帮助在上下文中理解MVVM的概念。
我用VS2008SP1创建的这个演示程序,框架是Microsoft.NETFramework3.5SP1。
单元测试是用的VisualStudiounittesting。
应用可以包含任意数量的“Workspace”,每一个都可以由用户点击左侧导航区的命令链接打开。
所有的Workspace寄宿在主区域TabControl中,用户可以通过点击workspace的tabitem上关闭按钮关闭workspace。
应用程序有两个可用的workspace:
"AllCustomers"和"NewCustomer"。
运行程序,打开一些workspace,UI看起来如图1所示。
图1Workspaces
一次只有一个“AllCustomers“Workspace的实例可以打开,但是可以打开多个NewCustomerWorkspace。
当用户决定创建一个新的客户时,她必须填完图2所示的数据输入表单。
图2新客户数据输入表单
填完数据输入表单的所有有效值点击“Save”按钮,新客户的名称将会出现在tabitem上面,同时新客户也会增加到客户列表中。
应用程序不支持删除或者编辑客户,但是这和其它功能类似,很容易在已有的程序架构上去实现。
现在你已经对演示程序有了更深层次的理解了,接下来我们研究它是如何设计以及实现的。
中继命令逻辑(RelayingCommandLogic)
除了类构造器里调用初始化组件标准的样板代码,应用中的每一View的codebehind文件都是空的。
实际上你可以移除View的codebehind文件,程序让人能够争正确的编译和运行。
尽管View中没有事件处理方法,但是当用户点击按钮时,程序依然能够响应并满足用户的请求。
之所以这样,是因为UI上Hyperlink、Button以及MenuItem控件的Command属性被绑定了。
绑定机制确保当用户在控件上点击时,由ViewModel暴露的ICommand对象能够执行。
你可以把command对象看作一个适配器,这个适配器让command对象很容易消费在View中声明的ViewModel功能。
当ViewModel暴露ICommad类型的实例属性,被暴露的Command对象使用ViewModel中的对象去完成它的工作。
其中一个可能的实现模式是在ViewModel内创建一个私有嵌套类,以便command能够访问包含在ViewModel中的私有成员,而不至于污染命名空间。
嵌套类实现了ICommand接口,包含在ViewModel中对象的引用注入到其构造器中。
但是为ViewModel暴露的每个Command创建实现ICommad的嵌套类,会增加ViewModel类的大小。
更多的代码意味着存在BUGS潜力更大。
在演示程序中,RelayCommand类解决了这个问题。
RelayCommand允许通过把委托传给其构造器,以实现对命令逻辑的注入。
这种方式允许在ViewMode类中可以简单明了的实现Command。
RelayCommand是DelegateCommand的一个简单的变体,DelegateCommand可以在MicrosoftCompositeApplicationLibrary找到。
RelayCommand类代码如图3所示。
图3RelayCommand类
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
}
作为接口ICommad实现一部分,事件CanExecuteChanged有一些值得关注的特征。
它委托订阅CommandManager.RequerySuggested事件。
这样以确保无论何时调用内置命令时,WPF命令架构都能调用所有能够执行的RelayCommand对象。
RelayCommand_saveCommand;
publicICommandSaveCommand
{
get
{
if(_saveCommand==null)
{
_saveCommand=newRelayCommand(param=>this.Save(),
param=>this.CanSave);
}
return_saveCommand;
}
}
ViewModel类层级图
大部分ViewModel类有共同的特征,他们要实现INotifyPropertyChanged接口,需要显示一个友好的名字,以之前说道Workspace为例,它需要能够关闭(即从UI上移除)。
要解决这个问题,自然就需要创建一个或二个ViewModel基类,以便新的ViewModel类能够从基类集成通用的功能。
所有的ViewModel类形成如图4的层级图。
图4继承层级图
为你的ViewModel创建一个基类并不是必须。
如果你喜欢在类中通过组合几个小一点的类以获得那些功能,而不是用继承的方式,这并没有什么问题。
就像任何其他的设计模式一样,MVVM是一套指导方针,而不是规则。
ViewModelBase类
ViewModelBase是层级中的根类,这就是它要实现通用INotifyPropertyChanged接口以及有一个DisplayName属性的原因。
INotifyPropertyChanged接口包含一个叫PropertyChanged的事件。
无论何时ViewModel对象的属性的发生改变时,它都会触发PropertyChanged事件,把新值通知给WPF绑定系统。
根据通知,绑定系统检索属性,UI组件上绑定的属性将接受新值。
为了让WPF知道是那一个属性发生了改变,PropertyChangedEventArgs类暴露了一个string类型的属性PropertyName。
你一定要为事件参数传递正确的属性名,否则WPF将会为新值检索出一个错误的属性。
ViewModelBase一个值得关注的地方就是它为给定的属性名提供了验证,验证属性是否存在ViewModel对象上。
重构时,这非常有用。
因为通过VS2008重构功能去改变属性名,不会更新源代码中字符串,而这些字符串正好包含属性名(其实不应该包含)。
在事件参数中传递不正确的属性名,触发PropertyChanged事件时,可能会导致微小的BUGs,并且这些BUGs很难追踪,因此这个细微的特征将会节省大量的时间。
ViewModelBase中增加了这个有用的特征,其代码如下:
图5属性验证
//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类
CommandViewModel是最简单的ViewModelBase子类,它暴露了一个类型为ICommad的Command属性。
MainWindowViewModel通过Commands属性暴露了CommandViewModel对象的一个集合。
主窗口左手侧的导航区域,显示了MainWindowViewModel暴露每个CommandViewModel对象链接,像“Viewallcustomers”和“Createnewcustomer”。
当用户点击链接,将会执行相应的Command,在主窗口的TabControl中打开一个workspace。
CommandViewModel类的定义如下所示:
publicclassCommandViewModel:
ViewModelBase
{
publicCommandViewModel(stringdisplayName,ICommandcommand)
{
if(command==null)
thrownewArgumentNullException("command");
base.DisplayName=displayName;
this.Command=command;
}
publicICommandCommand{get;privateset;}
}
在MainWindowResources.xaml文件中存在一个key为CommandsTemplate的数据模版,主窗口(MainWindow)使用这个模版渲染之前提到的CommandViewModel对象集合。
这个模版是简单在ItemsControl里把每个CommandViewModel对象渲染成一个链接,每个链接的Command属性绑定到CommandViewModel对象的Command属性。
数据模版Xaml如图6所示:
图6渲染Command列表
--InMainWindowResources.xaml-->
--
Thistemplateexplainshowtorenderthelistofcommandson
theleftsideinthemainwindow(the'ControlPanel'area).
-->
Key="CommandsTemplate">