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實例中的實現,以下圖所示:
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,例如,每個買/賣的 SubmitCancel命令註冊到 SubmitAllOrders命令中以及 CancelAllOrder命令中。以下示例(查看 OrdersConttoller類):
C# OrdersController.cs
commandProxy.SubmitAllOrdersCommand.RegisterCommand(
                        orderCompositeViewModel.SubmitCommand );
commandProxy.CancelAllOrdersCommand.RegisterCommand(
   
注意:
上面的 CommandProxy對象提供了訪問 SubmitCancel組合命令的實例,它被定義爲靜態對象。更多信息,查看StockTraderRICommands.cs文件
執行子視圖的命令
    常常,你的應用程序須要在UI上展現一個子View的集合,每一個子View將會有一個一致的ViewModel,依次,可能實現了一個或多個命令。組合命令能夠用來展示這些在UI中的子View實現而且整合瞭如何被父View中調用的命令。爲了支持這種場景,Prism設計了同Region一塊兒的 CompositeCommandDelegateCommand類。
    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模式」中「從視圖調用命令方法」一節中描述的 InvokeCommandActionCallMethodAction,而且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,而是使用 BeginGetQuestionnaireEndGetQuestionnaire的一對兒方法。爲了啓動異步調用,你會調用 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類在應用程序中當交互請求隊形用於通知用戶重要事件時被使用。它提供了兩個屬性--- TitleContent---它們將會展現給用戶。一般通知是單向的,因此將不會指望用戶會在交互過程當中改變這些值。
     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窗口同事也包含了 OkCancel按鈕來捕獲用戶的應答。你能夠經過指定 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一般會調用服務的操做,常常異步的方式調用。調用這些服務的測試代碼用模擬或做爲替代實際服務存根。
    用於實現異步操做的標準模式提供關於通知操做發生的狀態的線程不一樣的保證。雖然 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).aspxapp

關於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."async

關於使用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

相關文章
相關標籤/搜索