Prism 4 文档 ---第6章 高级MVVM场景


    在上一章中描述了如何通过将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实例中的实现,如下图所示:

    这种方法提供了一种简单而灵活的机制,保持视图模型和完全分离视图,它允许ViewModel来封装应用程序的显示逻辑,包括任何所需的用户交互,同时允许View以完全封装的视觉交互的多个方面。ViewModel的实现,包括它期望的用户通过View的交互,可以很容易地进行测试,并且UI设计师在选择如何通过使用封装了不同用户体检的交互的行为实现View的交互时有很大的灵活性。     这种方式是和MVVM的方式一致的,使得View可以反映其观测的ViewModel的状态变化并且利用双向绑定来实现两者之间的数据通信。交互请求对象中封装了不可视元素的交互,并且使用相应的行为管理交互的可视化元素,这种方式同命令对象与命令行为的使用方式非常相似。     Prism采用了这种方法。Prism类库通过IInteractionRequest接口和 InteractionRequest 类直接支持了这种模式。IInteractionRequest接口定义了一个事件来发起交互。View中的行为绑定到了这个接口,并且订阅了它暴露的事件。 InteractionRequest 类实现了IInteractionRequest接口并且定义了两个Raise方法使得ViewModel发起一个交互并且指定上下文的要求,以及可选的回调委托。  从View Model初始化交互请求

    InteractionRequest 类在交互请求期间匹配了View和View Model的交互 。Raise方法使得ViewModel发起交互并且指定上下文对象(类型为T的对象)和一个回调方法,这个方法在交互完成后才会被调用。上下文对象允许ViewModel将同用户交互过程中用到的数据和状态传递到View。如果指定了回调方法,上下文对象将会传递回ViewModel;这使得用户在交互过程中做的任何改变都能传递回ViewModel。

public interface IInteractionRequest
{
    event EventHandler Raised;
}
 
public class InteractionRequest : IInteractionRequest
{
    public event EventHandler Raised;
 
    public void Raise(T context, Action callback)
    {
        var handler = this.Raised;
        if (handler != null)
        {
            handler(
                this, 
                new InteractionRequestedEventArgs(
                    context, 
                    () => callback(context)));
        }
    }
}
    Prism提供了一个预定义上下文类来支持通常的交互请求场景。Notification类是所有上下文类的基类。Notification类在应用程序中当交互请求队形用于通知用户重要事件时被使用。它提供了两个属性---TitleContent---它们将会展示给用户。通常通知是单向的,所以将不会期望用户会在交互过程中改变这些值。     Confirmation类派生自Notification类并且添加了第三个属性---Confirmed---它被用来标识用户已经确认或者拒绝了操作。Confirmation类用来在想要获取用户是/否的回应的地方实现MessageBox式的交互。你可以定义一个派生自Notification类的自定义的上下文类来封装支持交互所需要的任何数据和状态。     使用InteractionRequest类,ViewModel类将会创建一个InteractionRequest类的实例并且定义一个只读的属性来使得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类来实现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窗口同事也包含了OkCancel按钮来捕获用户的应答。你可以通过指定PopupChildWindowAction类的ChildWindow属性来重新这个行为。     下面的示例展示了在MVVM RI中如何使用InteractionRequestTrigger 和 PopupChildWindowAction来给用户展示一个确认窗口。

    

        

    



    
        
            
        
    
注意:     使用指定的数据模板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();
    然后你可以通过容器实例化View,如下所示。
IUnityContainer container;
var view = container.Resolve();
使用扩展类创建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().Any());

// Valid case
var notifyErrorInfo = (INotifyDataErrorInfo)question;

question.Response = 15;
Assert.IsFalse(notifyErrorInfo.GetErrors("Response").Cast().Any());
   交叉属性验证规则遵循相同的模式,通常需要更多的测试来适应不同属性的值的组合。 测试INotifyDataErrorInfo实现的需求    除了为GetErrors方法生产正确的值,INotifyDataErrorInfo接口的实现中也必须保证ErrorsChanged事件被适当的引发。例如当GetErrors方法返回值不同时。另外,HasErrors属性必须反映实现了这个接口的对象的正格状态。     没有强制性的方法实现INotifyDataErrorInfo接口。然而,依赖对象的实现积累验证错误和执行必要的通知通常是首选的,因为它们测试很简单。这是因为没有必要验证所有实现了INotifyDataErrorInfo接口的成员的每个验证属性(当然,只要错误的管理对象是正确的测试)满足了每个验证规则的要求。 试的接口需求至少应该包括以下验证:
  • HasErrors属性反映了对象的整体错误状态。为前面的一个不合法的属性设置一个合法值时如果其他值仍然有非法值的话不会导致这个属性结果的改变。
  • 当一个属性的错误状态发生改变时,作为反映了GetErrors方法的结果,ErrorsChanged事件被引发,错误状态可以有正确状态(没有错误)到错误状态并且反之亦然,或者它可以由一个错误状态到另一个错误状态。GetErrors方法的更新后的结果对于ErrorsChanged事件是可用的。
当测试INotifyPropertyChanged接口的实现时,帮助类,例如MVVM 实例工程中的NotifyDataErrorInfoTestHelper类,通常通过处理重复的日常操作和标准检测使得编写INotifyDataErrorInfo接口的实现类的测试更简单。这在不基于任何可复用错误管理是实现接口时非常有用。下面的示例代码展示了这样的帮助类。
var helper = 
    new NotifyDataErrorInfoTestHelper(
        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(), 
                It.IsAny>()))
    .Callback>(
        (q, a) => callback = a);
 
uiServiceMock
    .Setup(svc => svc.ShowView(ViewNames.QuestionnaireTemplatesList))

    .Callback(viewName => requestedViewName = viewName);
submitResultMock
    .Setup(sr => sr.Error)
    .Returns(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更多信息,请参考第五章, "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章 "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