在上一章中描述了如何通过将UI,表现逻辑,业务逻辑分别放到三个单独的类中(View,View Model,Model),实现这些类之间的交互(通过数据绑定,命令以及数据验证接口)以及实现一个策略来处理建筑和绑定的方式实现MVVM的基本元素。
通过使用实现MVVM的这些基本元素的方式可以支持应用程序中许多的应用场景。然而,您可能会遇到更复杂的场景,需要扩展基本MVVM模式或者需要应用更先进的技术。如果你的应用程序比较大或者比较复杂,这种情况很有可能会发生,但也可能在很小的应用中遇到这些场景。Prism类库提供了许多已经实现了这些技术的组件,允许你可以更加容易的在应用程序中使用它们。
本章介绍了一些复杂的场景,并介绍了MVVM模式如何支持他们。下一节将说明如何命令可以链接在一起,或与子视图,以及他们如何可以扩展到支持自定义的要求。以下各节则描述了如何处理异步数据请求和随后的UI交互,以及如何处理的视图和视图模型之间的交互请求。
本节为提供了当使用依赖注入容器时处理构造方式和wire-up的指导,例如Unity或者使用MEF。最后一节介绍了如何通过单元测试您的应用程序的ViewModel和Model类提供指导测试MVVM应用程序,以及测试的行为。
命令
命令提供了将命令的实现逻辑从UI展现中分离出来的一种方式,数据绑定和行为提供了将View中声明的元素与ViewModel中提供的命令相关联的一种方式。在第5章实现MVVM模式中描述了如何在ViewModel中将命令实现为一个命令对象或者命令方法,以及如何通过行为或者与特定控件内联的命令属性在View中被调用的。
注意 :
WPF Routed Commands:需要注意的是在MVVM模式中奖命令实现为命令对象或者命令方法与WPF的内建的实现路由命令是有一些不同的(Sliverlight没有任何路由命令的实现).WPF路由命令通过路由遍历元素的方式在UI元素树(特指逻辑树)中来传递命令消息。因此,命令消息在UI树中是从焦点元素或者特定的目标元素向下或者向上路由传递的;默认的,它们不会路由遍历UI树的外部组件,例如与View关联的View Model。然而,WPF路由命令可以使用视图中定义一个命令处理程序的后台代码转发命令调用视图模型类。
组合命令
在许多情况下,在ViewModel中定义的一个命令将会被绑定到与关联View中控件,那样用户可以直接从View中调用命令。然而,在一些情况下,你可能想要在一个父类View中的控件调用一个或者多个ViewModel类中的命令。
例如,在你的应用程序中允许用户同事编辑多个条目,你可能想要允许用户通过应用程序中工具栏或者功能区中某个展现为一个按钮的命令来一次保存所有的条目。在这种情况下,Save All命令将会调用Save命令在每一个ViewModel实例中的实现,如下图所示:
Prism通过CompositeCommand类支持这种场景。
CompositeCommand类代表了一个来自多个子命令聚合在一起的命令。当一个组合的命令被调用时,每个子命令将会一次的被调用。它在你需要在UI中使用一个单独的命令代表一组命令或者您希望调用多个命令来实现逻辑命令的时候很有用。
例如,CompositeCommand在Stock Tarder RI中使用,目的是在买/卖View中通过展示一个Submit All按钮来实现SubmitAllOrders命令的功能。当用户点击Submit All按钮是,每个定义在不同的个人买/卖交易中的SubmitCommand将会被执行
CompositeCommand类维护着一系列的子命令(DelegateCommand实例)。CompositeCommand类的Execute方法只是简单的依次调用每个子命令的Execute方法。CanExecute方法也只是简单的调用每个子命令的CanExecute方法。但是如果任何一个子命令不能被执行,CanExecute将会返回false。换而言之,只有所有子命令可以被执行,CompositeCommand才可以被执行。
注册及卸载子命令
通过RegisterCommand和 UnregisterCommand方法来注册和卸载子命令。在Stock Trader RI,例如,每一个买/卖的Submit和Cancel命令注册到SubmitAllOrders命令中以及CancelAllOrder命令中。如下示例(查看OrdersConttoller类):
C# OrdersController.cs | |
---|---|
commandProxy.SubmitAllOrdersCommand.RegisterCommand( orderCompositeViewModel.SubmitCommand ); commandProxy.CancelAllOrdersCommand.RegisterCommand( |
注意:
上面的CommandProxy对象提供了访问Submit和Cancel组合命令的实例,它被定义为静态对象。更多信息,查看StockTraderRICommands.cs文件
执行子视图的命令
经常,你的应用程序需要在UI上展示一个子View的集合,每个子View将会有一个一致的ViewModel,依次,可能实现了一个或多个命令。组合命令可以用来展现这些在UI中的子View实现并且整合了如何被父View中调用的命令。为了支持这种场景,Prism设计了同Region一起的CompositeCommand和DelegateCommand类。
Prism Region(在第7章 组合用户界面中的“Regions”一节介绍)提供使得程序的子View和UI界面中的逻辑占位符联系在一起的一种方法。他们经常被用来将子View指定的布局方式与逻辑占位符和UI中的位置解耦。Regions是基于占位符名称来联系到指定的布局控件的。下面的插图示例中展示了每个子View被添加到名称为EditRegion的Region中,UI设计师在Region中选用Tab控件布局View。
复合命令在父view级别通常会被用来协调命令在子view级别是如何调用的。在一些情况下,你想要所有的显示View的命令被执行,就像在前面的Save All命令。在另外一些情况下,你想要仅在活跃View的视图中的命令被执行。在这种情况下,复合命令将会执行在被认为是活跃的View中的命令;那些在非活跃View中的命令将不会被执行。例如,你可能在应用程序工具栏或者功能区实现一个缩放功能的命令,它只会使得当前活动的View进行缩放,如下图所示:
为了支持这种场景,Prism 提供了IActiveAware接口,IActiveAware接口定义了一个IsActive属性,当它的实现者出在活跃状态时返回true,定义了一个活跃状态发生变化时将会引发的IsActiveChanged事件。
你可以在子View或者ViewModel上实现IActiveAware接口。它主要用于在Region中跟踪子View的状态。一个View是否处于活动状态决定与区域适配器(Region Adaper),它负责指定Region控件中的Views。例如,就像前面展示的Tab控件,它就有一个区域适配器来设置当前选中的View处在Active状态。
DelegateCommand类也实现了IActiveAware接口。通过在构造方法中指定monitorCommandActivity参数为true来配置CompositeCommand以评估它的子DelegateCommands的活动状态(除CanExecute状态外)。这个参数被设置为true时,当确定CanExecute方法的返回值以及当执行子命令的Execute方法时,CompositeCommand类将会考虑每个子DelegateCommand的活动状态。
当monitorCommandActivity参数为true时,CompositeCommand类展现以下行为:
- CanExecute。只有当所有的活动的命令可以被执行时,才会返回true。那些非活动的子命令将不会被考虑。
- Execute。执行所有的活动的命令。非活动的命令将不会被考虑。
你可以利用这个功能来实现前面的例子。通过在你的子ViewModel中实现IActiveAware接口,在Region中的子View的变成活动或者非活动时你都会被通知。当子View的状态改变时,你可以更新子命令的状态。然后,当用户调用Zoom复合命令时,活动的子View的Zoom命令将会执行。
集合命令
另一种常见的情况,你显示在视图中的项目集合时会经常遇到的是,当你需要的用户界面为每个项目集合中要与在父视图级别(而不是项目级)的命令有关。
例如,在如下图所示的应用程序中,视图显示项目的集合在一个ListBox控件,用于显示每个项的数据模板定义了一个删除按钮,允许用户删除从集合中的个别项目。
因为ViewModel实现了Delete命令,面临的挑战是要连接的Delete按钮在用户界面的每个项目,由ViewModel实现的Delete命令。
困难的产生是由于在ListBox中的每一项的数据上下文引用的集合中的项,而不是一个实现的删除命令中的父ViewModel中的项。
解决这个问题的一种方法是在数据模板中使用ElementName属性绑定父View中的命令,来保证绑定是相对于父控件,而不是相对于数据模板,下面的XAML展示了这种技术:
<Grid x:Name="root"> <ListBox ItemsSource="{Binding Path=Items}"> <ListBox.ItemTemplate> <DataTemplate> <Button Content="{Binding Path=Name}" Command="{Binding ElementName=root, Path=DataContext.DeleteCommand}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> |
数据模板中的按钮控件的内容绑定到了集合项中的Name属性。然而,按钮的命令通过使用root元素的数据上下文绑定到了Delete命令。这使得按钮的命令绑定到了父View级别而不是项目级别。你可以使用CommandParameter属性指定哪一项应用命令或者你可以实现命令来操作当前选中项(使用CollectionView)。
命令行为
在Sliverlight3和更早版本中,Silverlight中控件不直接支持命令。ICommand接口可以用。但是没有控件实现了Command属性来使得它们直接拥有ICommand实现的钩子。为了解决这个限制并在Silverlight3中支持MVVM模式,Prism类库(2.0版本)提供了一种通过附加属性机制来允许任何Silverlight控件绑定到命令对象。这种机制在WPF中同样起作用,这使得ViewModel实现可以在Silverlight和WPF应用程序之间复用。
接下来的例子展示了,Prism 命令对象是如何将一个按钮事件绑定到在ViewModle中定义的命令对象的。
<Button Content="Submit All" prism:Click.Command="{Binding Path=SubmitAllCommand}" prism:Click.CommandParameter="{Binding Path=TickerSymbol}" /> |
Silverlight4为所有Hyperlink派生控件和ButtonBase派生控件支持了Command属性,使得它们可以像在WPF中一样可以直接绑定到命令对象,在第5章 “实现MVVM模式”中的“命令”一节描述了这些控件的Command属性的使用。然而,Prism 命令行为仍然支持向后兼容,并且支持发展自定义行为,接下来会描述。
行为方式是一种通用可行的技术,用来实施并在某种程度上封装交互行为使得很容易被应用到View中的控件的一种方式。
扩展Prism命令行为,在前面的使用行为来支持命令仅是行为可以支持的许多场景之一。Blend已经提供了各种各样的行为,包括第5章“实现MVVM模式”中“从视图调用命令方法”一节中描述的InvokeCommandAction和CallMethodAction,并且SDK允许开发自定义行为。Blend提供了拖拽创建和属性编辑行为。这使得添加任务非常方便。关于更多开发自定义Blend行为的知识,请看MSDN上的“Creating Custom Behaviors"
虽然Silverlight4中引入了对命令的支持, 并且引入了Blend SDK中的行为,但是避免太多的必要性Prism命令的行为,你会发现他们的紧凑语法和实施,以及他们的能力可以很容易地扩展,是有用的。
扩展Prism命令行为
Prism命令行为是基于一个附加的行为模式。这种模式通过连接到控制的ViewModel所提供的命令对象引发的事件。Prism命令的行为是由两部分组成:一个附加的属性和行为对象。附加属性确定了目标控制和行为对象之间的关系。行为对象监视目标控件和采取基于事件的动作或控件状态的变化或者ViewModel。
Prism命令通过提供ButtonBaseClickCommandBehavior类和一个附加属性附加到目标控件的点击事件来执行基于ButtonBase派生控件的Click事件。下面的插图展示了ButtonBase,ButtonBaseClickCommandBehavior 和ViewModel提供的ICommand对象之间的关系。
你的应用程序可能需要从控件或者事件调用命令而不是从ButtonBase的Click事件,或者你可能需要自定义目标控件和绑定的View model之间的行为交互方式。在这种情况下,你将需要定义你自己的附加属性和/或行为实现。
Prism类库提供了CommandBehaviorBase<T>类使得创建同ICommand 对象交互的行为变得简单。这个类调用命令并且监视命令的CanExecuteChanged事件的变化,并且它可以用来在Silverlight和WPF中扩展命令。
为了创建自定义的行为,创建一个继承自CommandBehaviorBase<T>的类并且关联你需要监视的目标控件。这个类的参数指定了行为被附加的控件的类型。在你的类的构造方法中,你可以从你监视的控件订阅事件。下面的例子展示了是实现了ButtonBaseClickCommandBehavior的类。
public class ButtonBaseClickCommandBehavior : CommandBehaviorBase<ButtonBase> { public ButtonBaseClickCommandBehavior(ButtonBase clickableObject) : base(clickableObject) { clickableObject.Click += OnClick; } private void OnClick(object sender, System.Windows.RoutedEventArgs e) { ExecuteCommand(); } } |
使用CommandBehaviorBase<T>类,你可以定义你自己的自定义行为类;这允许你自定义目标控件和ViewModel提供的命令之间的行为交互。例如,你可以定义一个行为,它调用一个基于不同控件事件的命令或者改变一个基于绑定命令的CanExecute状态控件的可视化状态。
为了支持声明式将命令行为附加到目标控件,一个附加属性将会被使用。这个附加属性将允许在XAML中奖行为附加到控件上,并且管理构造方法和关联目标控件与行为实现。这个附加属性被定义在一个静态类中。Prism命令行为是基于公约,静态类指的是事件的名称,用于调用命令。附加的属性的名称是指被数据绑定的对象的类型。因此,前面描述的Prism命令行为使用一个名为Click的静态类,它定义了一个附加属性命名Command。这允许使用Click.Command语法所示。
命令行为对象本身其实也通过一个附加属性与目标控件相关联。然而,这个附加属性私有静态类和开发人员不可见。
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached( "Command", typeof(ICommand), typeof(Click), new PropertyMetadata(OnSetCommandCallback)); private static readonly DependencyProperty ClickCommandBehaviorProperty = DependencyProperty.RegisterAttached( "ClickCommandBehavior", typeof(ButtonBaseClickCommandBehavior), typeof(Click), null); |
实现命令的附加属性创建ButtonBaseClickCommandBehavior类的一个实例,通过OnSetCommandCallback回调方法,如以下代码示例所示。
private static void OnSetCommandCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { ButtonBase buttonBase = dependencyObject as ButtonBase; if (buttonBase != null) { ButtonBaseClickCommandBehavior behavior = GetOrCreateBehavior(buttonBase); behavior.Command = e.NewValue as ICommand; } } private static void OnSetCommandParameterCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { ButtonBase buttonBase = dependencyObject as ButtonBase; if (buttonBase != null) { ButtonBaseClickCommandBehavior behavior = GetOrCreateBehavior(buttonBase); behavior.CommandParameter = e.NewValue; } } private static ButtonBaseClickCommandBehavior GetOrCreateBehavior( ButtonBase buttonBase ) { ButtonBaseClickCommandBehavior behavior = buttonBase.GetValue(ClickCommandBehaviorProperty) as ButtonBaseClickCommandBehavior; if ( behavior == null ) { behavior = new ButtonBaseClickCommandBehavior(buttonBase); buttonBase.SetValue(ClickCommandBehaviorProperty, behavior); } return behavior; } |
关于附加属性的更多信息,请参阅附加属性在MSDN概述。
处理异步交互
你的ViewModel将会经常需要同应用程序的服务和组件进行异步的通信交互而不是同步交互。这将非常场景如果你在创建一个Sliverlight应用程序或者同一个Web Service进行交互 或者通过网络访问其他资源,或者是你的应用程序使用后台任务来执行计算或者I/O。异步执行这些操作可以保证你的应用程序仍能响应这对于提供一个良好的用户体验是关键的。
当用户启动一个异步请求或者后台任务,预测何时响应将会到达(或者它是否会到达)是非常困难的,通常,它将会返回哪个线程。因为UI只能在UI线程中更新,你需要经常通过调度请求在UI线程中更新UI。
检索数据和与Web Service 交互
当同Web Services或者其他的远程访问技术交互的时候,你会经常遇到IAsyncResult模式。在这种模式中,不会调用一个方法,像GetQuestionnaire,而是使用BeginGetQuestionnaire和EndGetQuestionnaire的一对儿方法。为了启动异步调用,你会调用BeginGetQuestionnaire。为了获取结果或者当发生异常时决定合适调用一个目标方法,你需要在调用完成时调用EndGetQuestionnaire
为了确定何时调用EndGetQustionnaire,你最好在调用完成时或者在调用BeginGetQuestionnaire中指定一个回调。使用回调的方式,你的回调方法将会在目标方法执行完成时被调用,使得你从那里调用EndGetQuestionnaire方法,如下所示:
IAsyncResult asyncResult = this.service.BeginGetQuestionnaire(GetQuestionnaireCompleted, null // object state, not used in this example); private void GetQuestionnaireCompleted(IAsyncResult result) { try { questionnaire = this.service.EndGetQuestionnaire(ar); } catch (Exception ex) { // Do something to report the error. } } |
需要注意的是在调用End方法(在此指的,EndGetQuestionnaire),执行过程中发生的任何异常都会被引发。应用程序必须处理这些情况并且需要使用UI在一个线程安全的方式报告它们。如果你不处理这些异常,这个线程将会结束并且你讲不能继续处理这些结果。
由于应答通常并非在UI线程中,如果你计划修改的任何东西会影响UI的状态的话,你要么使用Dispatcher线程要么使用SynchronizationContext对象来调度以展示到UI线程上。在WPF和Silverlight中,一般使用dispathcer。
在下面的示例代码中,Questionnaire对象是异步获得的,并且它被设置为QuestionnaireView的数据上下文。在Silverlight中,你可以使用dispathcer的CheckAcess方法的检测是目前否拥有UI线程的访问权。如果不允许访问,你讲需要使用BeginInvoke方法将请求放到UI线程中。
var dispatcher = System.Windows.Deployment.Current.Dispatcher; if (dispatcher.CheckAccess()) { QuestionnaireView.DataContext = questionnaire; } else { dispatcher.BeginInvoke( () => { Questionnaire.DataContext = questionnaire; }); } |
MVVM RI展示了一个类似前面例子的如何使用基于IAsyncResult服务接口的示例,它同时也包装了这个服务为消费者提供了一个简单的回调机制以及在调用线程中处理回调方法的调度。例如,下面的示例展示了questionnaire的获取。
this.questionnaireRepository.GetQuestionnaireAsync( (result) => { this.Questionnaire = result.Result; }); |
result对象返回了除了获取的结果外还有可能发生的错误的一个封装。下面的代码展示了如何评估处理错误。
this.questionnaireRepository.GetQuestionnaireAsync( (result) => { if (result.Error == null) { this.Questionnaire = result.Result; ... } else { // Handle error. } }) |
用户交互模式
经常,应用程序需要通知用户某件事的发生情况或者在处理一个操作之前请求用户的确认。应用程序中这些简洁的交互在经常设计成变化的一个简单的通知或者获取一个简单的回应。一些交互对用户来说可能是模态的,比如展示一个对话框或者一个消息框,也有可能展现给用于一个非模态的,比如显示一个举杯通知或者弹出窗口。
在这种情况下有多种同用户交互的方式,但是在一个基于MVVM的应用程序中实现交互的方式保持一个清楚的分离关注点将会是很具挑战的。例如,在一个非MVVM的应用程序中,经常在UI的后台代码中使用MessageBox来获取用户的应答。在一个MVVM应用程序中,这样就不太合适了,因为这将会打破View和View Model之间的关注点分离。
根据MVVM模式,ViewModel负责初始化用户的交互以及消费和处理任何应答,View负责真实的管理通用户的交互无论何种用户体验。保持在ViewModel中实现的展示逻辑和View中实现的用户体验的关注点的分离有助于提示应用程序的可测试性和灵活性。
在MVVM模式中有两种实现这类交互的方式。一种方式是实现可以被ViewModel用于发起同用户交互的服务,因此需要保持它独立与View的实现。另一种方式是使用ViewModel引发的实现来表达同用户的交互,随着View组件绑定到这些事件管理这交互的可视化方面。每一个这种方式将会在下面的章节中讲述。
使用一个交互服务
在这种方式中,ViewModel依赖于一个交互服务组件通过使用消息对话框发起同用户的交互。这种方式支持通过将可视化交互的实现封装到单独的服务组件中提供了关注点的清晰的分离以及可测试性。通常,ViewModel有一个交互服务的依赖,它通常用于通过一个依赖注入或者服务定位器获取交互服务的引用。
在View Model拥有交互服务的引用后,它可以在任何时候通过编程的方式同用户进行交互。交互服务实现了交互的可视化方面,如下面的插图所示。在ViewModel中使用一个接口引用允许使用根据用户接口需求的不同的实现。例如,使用WPF和Silverlight提供的交互的实现,可以更多的复用应用程序的展现逻辑。
模态交互,例如展现给用户一个MessageBox或者以模态的弹出窗口在程序可以执行之前来获取指定的应答,可以使用一个方法快的调用以同步的方式实现,如下面的示例:
var result = interactionService.ShowMessageBox( "Are you sure you want to cancel this operation?", "Confirm", MessageBoxButton.OK ); if (result == MessageBoxResult.Yes) { CancelRequest(); } |
然而,这种方式的一个缺点就是它强制使用一种同步编程的模式,这种模式不能被其他一系列Silverlight交互服务接口的不同实现结果的机制共享。一种可选的异步实现使得ViewModel提供一个在交互完成是执行的回调方法。下面的代码展示了这种方式。
interactionService.ShowMessageBox( "Are you sure you want to cancel this operation?", "Confirm", MessageBoxButton.OK, result => { if (result == MessageBoxResult.Yes) { CancelRequest(); } }); |
这种异步的方式在以模态和非模态的方式实现交互接口的时候实提供了更大的灵活性。例如,在WPF中,MessageBox类可以用于实现一个真正的模态与用户交互;然而,在Silverlight中,一个弹出窗口可以用于一种假模态与用户交互。
使用一个交互请求对象
MVVM模式中另一种实现的简单的用户交互方法是通过一个View中的行为的交互请求对象让ViewModel直接与View本身发生交互请求。交互请求对象封装的交互请求的详细信息,以及它的响应,并通过事件同View进行通信。View订阅了这些事件来发起交互中的用户体验部分。View通常将用户体验交互封装到一个行为中,这个行为绑定到了View Model提供的交互请求对象,就像下面插图所示。
这种方法提供了一种简单而灵活的机制,保持视图模型和完全分离视图,它允许ViewModel来封装应用程序的显示逻辑,包括任何所需的用户交互,同时允许View以完全封装的视觉交互的多个方面。ViewModel的实现,包括它期望的用户通过View的交互,可以很容易地进行测试,并且UI设计师在选择如何通过使用封装了不同用户体检的交互的行为实现View的交互时有很大的灵活性。
这种方式是和MVVM的方式一致的,使得View可以反映其观测的ViewModel的状态变化并且利用双向绑定来实现两者之间的数据通信。交互请求对象中封装了不可视元素的交互,并且使用相应的行为管理交互的可视化元素,这种方式同命令对象与命令行为的使用方式非常相似。
Prism采用了这种方法。Prism类库通过IInteractionRequest接口和 InteractionRequest<T> 类直接支持了这种模式。IInteractionRequest接口定义了一个事件来发起交互。View中的行为绑定到了这个接口,并且订阅了它暴露的事件。 InteractionRequest<T> 类实现了IInteractionRequest接口并且定义了两个Raise方法使得ViewModel发起一个交互并且指定上下文的要求,以及可选的回调委托。
从View Model初始化交互请求
InteractionRequest<T> 类在交互请求期间匹配了View和View Model的交互 。Raise方法使得ViewModel发起交互并且指定上下文对象(类型为T的对象)和一个回调方法,这个方法在交互完成后才会被调用。上下文对象允许ViewModel将同用户交互过程中用到的数据和状态传递到View。如果指定了回调方法,上下文对象将会传递回ViewModel;这使得用户在交互过程中做的任何改变都能传递回ViewModel。
public interface IInteractionRequest { event EventHandler<InteractionRequestedEventArgs> Raised; } public class InteractionRequest<T> : IInteractionRequest { public event EventHandler<InteractionRequestedEventArgs> Raised; public void Raise(T context, Action<T> callback) { var handler = this.Raised; if (handler != null) { handler( this, new InteractionRequestedEventArgs( context, () => callback(context))); } } } |
Prism提供了一个预定义上下文类来支持通常的交互请求场景。Notification类是所有上下文类的基类。Notification类在应用程序中当交互请求队形用于通知用户重要事件时被使用。它提供了两个属性---Title和Content---它们将会展示给用户。通常通知是单向的,所以将不会期望用户会在交互过程中改变这些值。
Confirmation类派生自Notification类并且添加了第三个属性---Confirmed---它被用来标识用户已经确认或者拒绝了操作。Confirmation类用来在想要获取用户是/否的回应的地方实现MessageBox式的交互。你可以定义一个派生自Notification类的自定义的上下文类来封装支持交互所需要的任何数据和状态。
使用InteractionRequest<T>类,ViewModel类将会创建一个InteractionRequest<T>类的实例并且定义一个只读的属性来使得View与之绑定。当ViewModel想要发起一个请求时,它将会调用Raise方法,并且传递上下文对象和可选的回调委托。
public IInteractionRequest ConfirmCancelInteractionRequest { get { return this.confirmCancelInteractionRequest; } } this.confirmCancelInteractionRequest.Raise( new Confirmation("Are you sure you wish to cancel?"), confirmation => { if (confirmation.Confirmed) { this.NavigateToQuestionnaireList(); } }); } |
MVVM RI示例在一个测量程序中阐述了如何使用IInteractionRequest接口和 InteractionRequest<T>类来实现View和ViewModel之间的用户交互。(查看QuestionnaireViewModel.cs文件)。
使用行为实现用户交互习惯
因为交互请求对象代表了一个交互逻辑,精确的用户交互体验定义在了View中。行为经常用于封装一个交互的用户体验;这使得UI设计师在ViewModel中选择一个合适的行为以及绑定一个交互请求对象。
View必须设置一个检测交互请求的事件,然后提供合适的可视化请求。Blend行为框架通过触发器和动作(triggers and actions)支持这种概念。当一个指定的事件发生时触发器用来启动一个动作。
Blend提供的标准的EventTrigger可以通过绑定到View。这就减少了Model暴露的交互请求对象来监视一个交互请求事件。然而,Prism类库定义了一个自定义的EventTrigger,名称是InteractionRequstrigger,它可以自动的连接IInteractionRequest接口的合适的事件,这就减少了扩展XAML的所需要的量,并且减少了无意的进入一个错误事件名。
当事件被引发之后,InteractionRequestTrigger 将会调用指定的动作。对于Sliverlight,Prism类库提供了PopupChildWindowAction类,它展示一个弹出的窗口给用户。当这个子窗口展现后,它的数据上下文将设置为交互请求对象的上下文参数。使用ContentTemplatePopupChildWindowAction类的属性,你可以指定一个数据模板来定义要使用的UI布局的内容属性上下文对象。弹出窗口的标题是绑定到上下文对象的标题属性。
注意:
默认情况下,PopupChildWindowAction类展示的弹出窗口的指定类型依赖于上下文对象的类型。对于一个Notifycation上下文对象,将会展示一个NotificationChildWindow类型的窗口,但是对于一个Confirmation上下文对象,则会展示一个ConfirmationChildWindow类型的窗口。NotificationChildWindow类型的创建只是简单的弹出一个窗口来展示通知信息,但是ConfirmationChildWindow窗口同事也包含了Ok和Cancel按钮来捕获用户的应答。你可以通过指定PopupChildWindowAction类的ChildWindow属性来重新这个行为。
下面的示例展示了在MVVM RI中如何使用InteractionRequestTrigger 和 PopupChildWindowAction来给用户展示一个确认窗口。
<i:Interaction.Triggers> <prism:InteractionRequestTrigger SourceObject="{Binding ConfirmCancelInteractionRequest}"> <prism:PopupChildWindowAction ContentTemplate="{StaticResource ConfirmWindowTemplate}"/> </prism:InteractionRequestTrigger> </i:Interaction.Triggers> <UserControl.Resources> <DataTemplate x:Key="ConfirmWindowTemplate"> <Grid MinWidth="250" MinHeight="100"> <TextBlock TextWrapping="Wrap" Grid.Row="0" Text="{Binding}"/> </Grid> </DataTemplate> </UserControl.Resources> |
注意:
使用指定的数据模板ContentTemplate属性定义了内容的UI布局属性的上下文对象。在前面的代码中,内容属性是一个字符串,所以TextBlock只是绑定到属性本身的内容。
作为用户与弹出窗口交互,根据上下文对象更新绑定中定义弹出窗口或数据模板用于显示的内容属性上下文对象。用户关闭弹出窗口后,上下文对象传递回ViewModel,连同任何更新的值,通过回调方法。MVVM RI中使用的确认的示例,默认的确认View中,单击OK(确定)按钮时,负责提供的确认对象的确认属性设置为true。
不同的触发器和动作可以用来定义支持其他的交互方式。Prism的InteractionRequestTrigger 和PopupChildWindowAction 类实现可以用来作为开发自己的触发器和动作的基类。
高级构造及Wire-Up
为了成功的实现MVVM模式,你将需要完全的理解View,Modle,ViewModle类的职责,那样你才能在正确的类中实现应用程序的代码。实现正确的模式,允许这些类进行交互(通过数据绑定、命令交互请求,等等)也是一个重要的要求。最后一步是考虑View,ViewModel 和Model类在运行时实例化并相互关联。
选择一个适当的策略来管理这一步尤为重要。如果你在应用程序中使用依赖注入容器。MEF和Unity都提供指定View,ViewModel和Modle之间的依赖关系的能力,和在运行时由容器实现它们。
通常,定义ViewModel为View的依赖,那样的话当View构建(使用容器)的时候它将自动的实现它需要的ViewModel。依次,ViewModel所依赖的任何组件和服务也会被容器进行实例化。在ViewModel被成功的实例化后,View将它设置为其数据上下文。
使用MEF创建View和ViewModel
使用MEF,你可以通过使用import属性指定一个View依赖于某个ViewModel,并且你可以使用Export属性指定具体的ViewModel被实例化的类型。你可通过使用一个属性或者作为一个构造参数来把ViewModel引入View。
例如,在MVVM RI中的QuestionnaireView中,为ViewModel声明了一个具有import属性的只写属性。当View实例化时,MEF创建了一个合适的ViewModel实例并设置为此属性的值。属性节点设置ViewModel为View的数据上下文,如下所示:
[Import] public QuestionnaireViewModel ViewModel { set { this.DataContext = value; } } |
ViewModel定义和导出属性如下所示:
[Export] public class QuestionnaireViewModel : NotificationObject { ... } |
定义一个importing constructor是可选的,如下所示:
public QuestionnaireView() { InitializeComponent(); } [ImportingConstructor] public QuestionnaireView(QuestionnaireViewModel viewModel) : this() { this.DataContext = viewModel; } |
注意:
你可以在MEF和Unity中使用属性注入和构造注入;然而,你可以能会发现属性注入与以上非常相似因为你不需用维护两个构造方法。实时设计工具,比如Visual Studio和Expression Blend,为了在设计器中展示它们,而需要控件有一个默认的无参的构造方法。你定义的任何额外的构造方法都应该保证会调用无参构造方法,那样View才能通过InitializeComponent方法正确的初始化。
使用Unity创建View和ViewModel
使用Unity作为依赖注入容器与使用MEF非常相似,而且都支持基于属性和基于构造方法的依赖注入。主要的区别就是在运行时类型通常不会隐式的发现;而是,它们必须注册到容器中。
通常,你在ViewModel中定义一个接口,那样ViewModel的具体类型将会从View中解耦,例如。View可以在ViewModel中使用一个构造参数来定窑它的依赖关系,如下所示。
public QuestionnaireView() { InitializeComponent(); } public QuestionnaireView(QuestionnaireViewModel viewModel) : this() { this.DataContext = viewModel; } |
注意:
默认的无参构造方法对于在一个实时设计工具中工作是必须的,例如Visual Studio和Expression Blend.
可选的,你可以在View中定义一个只写属性。Unity将会实例化所需要的ViewModel并在View实例化之后调用属性节点设置器数据上下文。
public QuestionnaireView() { InitializeComponent(); } [Dependency] public QuestionnaireViewModel ViewModel { set { this.DataContext = value; } } |
ViewModel类型将会注册到容器中,如下所示。
IUnityContainer container; container.RegisterType<QuestionnaireViewModel>(); |
然后你可以通过容器实例化View,如下所示。
IUnityContainer container; var view = container.Resolve<QuestionnaireView>(); |
使用扩展类创建View和ViewModel
经常,你会发现定义一个控制器或者服务类来协调View和ViewModel类之间的实例是非常有用的。这可以使用一个依赖注入容器来实现,比如MEF或者Unity,或者当View显示创建它所必须的ViewModel的时候。
在你的应用程序中实现导航时,这种方法是非常有用的。在这种情况下,该控制器被用在UI中的占位符控件或区域相关联,它负责将View的构建并将View映射到对应的占位符或者区域。
例如。MVVM RI通过一个容器使用了一个服务类来构建Views并且将他们显示在主页面中。在这个示例中,Views通过它们的名称指定。导航是通过调用一个UI服务中的ShowView方法来发起的,如下所示。
private void NavigateToQuestionnaireList() { // Ask the UI service to go to the "questionnaire list" view. this.uiService.ShowView(ViewNames.QuestionnaireTemplatesList); } |
在应用程序的UI中UI服务和一个占位符控件相关联;它封装了所需的View的创建和协调着在UI中的呈现。UIService类的ShowView方法通过使用容器(目的是它的ViewModel和其他依赖可以被完全的实例化)创建了View的实例并且将他们展示在合适的位置。如下所示。
public void ShowView(string viewName) { var view = this.ViewFactory.GetView(viewName); this.MainWindow.CurrentView = view; } |
注意:
Prism通过区域为导航提供了广泛的支持。区域导航使用了一种与之前实现方式相似的机制,除了区域管理这负责这协调实例关系和安放指定的视图到区域中。更过信息请看第8章“导航”中的“基于导航的视图”一节。
测试MVVM应用程序
测试MVVM应用程序的Models和ViewModels和测试其他类是相同的,并且使用相同的测试工具和测试技术例如单元测试和模拟框架可以被使用。这里有一些测试模式通常可以用于测试Model和ViewModel类并且可以从标准的测试技术和测试帮助类中获益。
测试INotifyPeropertyChanged实现
实现INotifyPeropertyChanged接口使得View可以对于源于Models和ViewModels的变化做出反映。这些变化不仅仅限于控件展示的本地数据;它们也用于控制View,就像ViewModel中状态引起启动动画或者控件是否不可用。
简单情况
可以直接通过测试代码进行更新的属性可以通过附加一个事件处理程序PropertyChanged事件,并检查该属性设置新值后,是否引发进行事件。
计算和非设置的属性。然而帮助类,例如用于简单的MVVM项目中的ChangeTracker类,可以用于附加一个处理程序并收集结果;这样就避免的在写测试代码时的重复的任务。下面的代码示例展示了一个使用此帮助类的测试。
var changeTracker = new PropertyChangeTracker(viewModel); viewModel.CurrentState = "newState"; CollectionAssert.Contains(changeTracker.ChangedProperties, "CurrentState"); |
通过代码生成器生成的属性保证了对INotifyPeropertyChanged接口的实现,例如通过Modle设计器设计生成的代码,通常情况下可以不必测试。
计算和不可设置的属性
当属性不能被测试代码设置时,例如只读属性或者非公共属性,计算而来的属性,需要刺激被测试对象的测试代码引起的变化属性及其相应的通知。然而,测试相同的结构,简单的情况下,如以下代码示例所示,改变一个Model对象会导致属性在一个ViewModel改变。
var changeTracker = new PropertyChangeTracker(viewModel); var question = viewModel.Questions.First() as OpenQuestionViewModel; question.Question.Response = "some text"; CollectionAssert.Contains(changeTracker.ChangedProperties, "UnansweredQuestions"); |
整个对象通知
当你实现了INotifyPeropertyChanged接口,它就允许一个对象使用null或者空字符串作为变化属性的名称引发PropertyChanged事件来表明整个对象的所有属性都可能发生了变化。这种情况可用于测试个别的属性名称。
测试INotifyDataErrorInfo实现
这里有几种机制可用于对可用绑定执行输入的验证,例如当属性被设置时抛出异常,实现IDataErrorInfo接口,以及(在Silverlight中)实现INotifyDataErrorInfo接口。实现INotifyDataErrorInfo接口也用于更复杂的验证,因为它支持标识多个属性的每一个错误并且异步执行和交叉属性的验证,因此,它也需要测试。
有两方法需要测试INotifyDataErrorInfo接口的实现:测试验证规则被正确的实现和测试实现接口的需求,例如在GetErrors方法的结果不同时引发ErrorsChanged事件。
测试验证规则
验证逻辑通常测试比较简单,因为通常踏实一个输出依赖输入的自包含过程。每个属性之间的验证规则是相关联的,它们应该在使用有效值,无效值,边界值等等赋予被测试的属性名称后调用GetErrors方法的返回结果的基础上进行测试。如果验证逻辑是共享的,当表达验证规则声明性地使用注释的验证属性的数据,更详尽的测试可以集中在共享验证逻辑上,另一方面,自定义验证规则必须通过测试。
// Invalid case var notifyErrorInfo = (INotifyDataErrorInfo)question; question.Response = -15; Assert.IsTrue(notifyErrorInfo.GetErrors("Response").Cast<ValidationResult>().Any()); // Valid case var notifyErrorInfo = (INotifyDataErrorInfo)question; question.Response = 15; Assert.IsFalse(notifyErrorInfo.GetErrors("Response").Cast<ValidationResult>().Any()); |
交叉属性验证规则遵循相同的模式,通常需要更多的测试来适应不同属性的值的组合。
测试INotifyDataErrorInfo实现的需求
除了为GetErrors方法生产正确的值,INotifyDataErrorInfo接口的实现中也必须保证ErrorsChanged事件被适当的引发。例如当GetErrors方法返回值不同时。另外,HasErrors属性必须反映实现了这个接口的对象的正格状态。
没有强制性的方法实现INotifyDataErrorInfo接口。然而,依赖对象的实现积累验证错误和执行必要的通知通常是首选的,因为它们测试很简单。这是因为没有必要验证所有实现了INotifyDataErrorInfo接口的成员的每个验证属性(当然,只要错误的管理对象是正确的测试)满足了每个验证规则的要求。
试的接口需求至少应该包括以下验证:
- HasErrors属性反映了对象的整体错误状态。为前面的一个不合法的属性设置一个合法值时如果其他值仍然有非法值的话不会导致这个属性结果的改变。
- 当一个属性的错误状态发生改变时,作为反映了GetErrors方法的结果,ErrorsChanged事件被引发,错误状态可以有正确状态(没有错误)到错误状态并且反之亦然,或者它可以由一个错误状态到另一个错误状态。GetErrors方法的更新后的结果对于ErrorsChanged事件是可用的。
当测试INotifyPropertyChanged接口的实现时,帮助类,例如MVVM 实例工程中的NotifyDataErrorInfoTestHelper类,通常通过处理重复的日常操作和标准检测使得编写INotifyDataErrorInfo接口的实现类的测试更简单。这在不基于任何可复用错误管理是实现接口时非常有用。下面的示例代码展示了这样的帮助类。
var helper = new NotifyDataErrorInfoTestHelper<NumericQuestion, int?>( question, q => q.Response); helper.ValidatePropertyChange( 6, NotifyDataErrorInfoBehavior.Nothing); helper.ValidatePropertyChange( 20, NotifyDataErrorInfoBehavior.FiresErrorsChanged | NotifyDataErrorInfoBehavior.HasErrors | NotifyDataErrorInfoBehavior.HasErrorsForProperty); helper.ValidatePropertyChange( null, NotifyDataErrorInfoBehavior.FiresErrorsChanged | NotifyDataErrorInfoBehavior.HasErrors | NotifyDataErrorInfoBehavior.HasErrorsForProperty); helper.ValidatePropertyChange( 2, NotifyDataErrorInfoBehavior.FiresErrorsChanged); |
测试异步服务调用
当实现MVVM模式时,ViewModel通常会调用服务的操作,经常异步的方式调用。调用这些服务的测试代码用模拟或作为替代实际服务存根。
用于实现异步操作的标准模式提供关于通知操作发生的状态的线程不同的保证。虽然Event-based Asynchronous design pattern的事件保证了这些事件的处理在应用程序中一个合适的线程中被调用,IAsyncResult design pattern并没有提供任何保证迫使原始调用的ViewModel代码,以确保将影响View的任何更改都发布到UI线程。
处理线程相关的要求更加复杂,因此,通常也难于使用代码测试。通常也需要测试代码本身也是异步的。当通知保证发生在UI线程,因为使用了标准的基于事件的异步模式或因为ViewModel依赖于服务访问层通知适当的线程,可以简化测试,可以基本上扮演“UI线程调度”的角色。
模拟服务的方式基于用于实现操作的异步事件模式。如果使用了一个基于方法的模式,服务接口的模拟创建一个标准的模拟框架通常就足够了,但是,如果使用了基于事件的模式,基于自定义类的模拟通常需要实现增加和删除处理服务时间的方法。
下面的示例代码展示了测试成功完成使用模拟服务在UI线程通知一个异步操作的适当的行为。在这个例子中,测试代码捕获了当调用一个异步服务时的ViewModel的回调应用。测试然后通过调用一个回调模拟了后来完整的调用。这种方式使得使用异步服务但是不会使得异步测试负责的方式测试一个组件。
questionnaireRepositoryMock .Setup( r => r.SubmitQuestionnaireAsync( It.IsAny<Questionnaire>(), It.IsAny<Action<IOperationResult>>())) .Callback<Questionnaire, Action<IOperationResult>>( (q, a) => callback = a); uiServiceMock .Setup(svc => svc.ShowView(ViewNames.QuestionnaireTemplatesList)) .Callback<string>(viewName => requestedViewName = viewName); submitResultMock .Setup(sr => sr.Error) .Returns<Exception>(null); CompleteQuestionnaire(viewModel); viewModel.Submit(); // Simulate callback posted to the UI thread. callback(submitResultMock.Object); // Check expected behavior – request to navigate to the list view. Assert.AreEqual(ViewNames.QuestionnaireTemplatesList, requestedViewName); |
注意:
用这种测试方法仅练习被测试对象的功能;它不测试的代码是线程安全的。
更多信息
关于逻辑树更多信息,请参考MSDN的 "Trees in WPF":
http://msdn.microsoft.com/en-us/library/ms753391.aspx
关于附加属性更多信息,请参考MSDN的 "Attached Properties Overview":
http://msdn.microsoft.com/en-us/library/cc265152(VS.95).aspx
关于MEF更多信息,请参考MSDN的 "Managed Extensibility Framework Overview" :
http://msdn.microsoft.com/en-us/library/dd460648.aspx.
关于Unity更多信息,请参考MSDN的 "Unity Application Block":
http://www.msdn.com/unity.
关于DelegateCommand更多信息,请参考第五章, "Implementing the MVVM Pattern."
关于使用Blend 行为的更多信息,请参考MSDN的 "Working with built-in behaviors":
http://msdn.microsoft.com/en-us/library/ff724013(v=Expression.40).aspx.
关于使用Blend创建自定义行为的更多信息,请参考MSDN的 "Creating Custom Behaviors":
http://msdn.microsoft.com/en-us/library/ff724708(v=Expression.40).aspx.
关于创建自定义触发器和动作的更多信息,请参考MSDN的"Creating Custom Triggers and Actions":
http://msdn.microsoft.com/en-us/library/ff724707(v=Expression.40).aspx.
关于WPF 和Sliverlight中调度程序更多信息,请参考MSDN的"Threading Model" and "The Dispatcher Class":
http://msdn.microsoft.com/en-us/library/ms741870.aspx
http://msdn.microsoft.com/en-us/library/ms615907(v=VS.95).aspx.
关于Sliverlight单元测试的更多信息,请参考"Unit Testing with Silverlight 2":
http://www.jeff.wilcox.name/2008/03/silverlight2-unit-testing/.
关于区域导航的更多信息,请参考 第8章 "Navigation."的 "View-Based Navigation"一节:
关于Event-based Asynchronous Pattern的更多信息,请参考MSDN的"Event-based Asynchronous Pattern Overview":
http://msdn.microsoft.com/en-us/library/wewwczdw.aspx
关于IAsyncResult design pattern的更多信息,请参考MSDN"Asynchronous Programming Overview":
http://msdn.microsoft.com/en-us/library/ms228963.aspx