在上一章中描述了如何经过将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进行缩放,以下图所示:html

为了支持这种场景,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回调方法,如如下代码示例所示。web
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类,它展现一个弹出的窗口给用户。当这个子窗口展示后,它的数据上下文将设置为交互请求对象的上下文参数。使用ContentTemplate
PopupChildWindowAction类的属性,你能够指定一个数据模板来定义要使用的UI布局的内容属性上下文对象。弹出窗口的标题是绑定到上下文对象的标题属性。
注意:
默认状况下,
PopupChildWindowAction类展现的弹出窗口的指定类型依赖于上下文对象的类型。对于一个
Notifycation上下文对象,将会展现一个
NotificationChildWindow类型的窗口,可是对于一个
Confirmation上下文对象,则会展现一个
ConfirmationChildWindow类型的窗口。
NotificationChildWindow类型的建立只是简单的弹出一个窗口来展现通知信息,可是
ConfirmationChildWindow窗口同事也包含了
Ok和
Cancel按钮来捕获用户的应答。你能够经过指定
PopupChildWindowAction类的
ChildWindow属性来从新这个行为。
下面的示例展现了在MVVM RI中如何使用I
nteractionRequestTrigger 和
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一般会调用服务的操做,常常异步的方式调用。调用这些服务的测试代码用模拟或做为替代实际服务存根。
处理线程相关的要求更加复杂,所以,一般也难于使用代码测试。一般也须要测试代码自己也是异步的。当通知保证发生在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); |
注意:
用这种测试方法仅练习被测试对象的功能;它不测试的代码是线程安全的。
更多信息