Prism初研究之使用Prism實現WPF的MVVM的高級應用

Prism初研究之使用Prism實現WPF的MVVM的高級應用


本章描述MVVM如何支持一些複雜的使用場景,以及如何組織命令和子視圖來知足用戶需求。本章還描述瞭如何處理異步數據請求以及以後的UI交互。

Commands

複合命令(Composite Commands)

一般,一個定義在View Model中的命令可以經過綁定到控件來實現直接命令調用。可是,有些狀況下,可能使用一個父視圖的控件調用一個或多個View Model的多個命令。
好比,若是應用程序容許用戶同時編輯多個數據項,就須要容許用戶經過點擊一個按鈕(命令)來保存全部的數據項。這種狀況下Save All命令將會調用每個數據項的Save命令。
實現Save All複合命令
Prism提供了CompositeCommand類來實現複合命令。
這個命令類由對個子命令組成。當複合命令被調用時,全部子命令將會依次調用。這個命令類能夠應用於使用一個邏輯命令調用多個命令和使用一個命令來表示一組命令兩種使用場景。
Stock Trader RI例子中的SubmitAllOrder命令就是一個複合命令。
CompositeCommand命令維護一個子命令(DelegateCommand實例)的列表,它的Execute方法只是遍歷調用了子命令的Execute。CanExecute方法相似,不過若是有一個子命令不可運行,就返回false。編程

註冊和註銷子命令

經過RegisterCommand和UnregisterCommand方法來註冊和註銷子命令。Stock Trader RI中每個訂單的Submit和Cancel命令都註冊到SubmitAllOrders和CancelAllOrders組件命令:設計模式

 
 
 
 
// OrdersController.cscommandProxy.SubmitAllOrdersCommand.RegisterCommand( orderCompositeViewModel.SubmitCommand );commandProxy.CancelAllOrdersCommand.RegisterCommand( orderCompositeViewModel.CancelCommand );

在活動的子視圖上運行命令

Prism的CompositeCommand和DelegateCommand類能夠與Prism的regions一塊兒工做。
下圖展現了一個子視圖如何添加到EditRegion的。UI設計者能夠選擇使用Tab控件在Region中佈局視圖:
用TabControl定義EditRegion
有時可能須要運行當前活動子視圖的命令:實現一個Zoom命令來引發活動視圖的縮放。

Prism提供了IActiveAware接口來支持這種使用狀況。該接口定義了一個IsActive屬性(在實現者活動是返回true)和一個IsActiveChanged事件(active狀態改變時發生)。
能夠在子視圖或者視圖模型類上實現IActiveAware接口。視圖的活動狀態由特定區域控件中的區域適配器(region Adapter)決定。上圖中,有一個region Adapter將選中標籤中的視圖設置爲活動的。
DelegateCommand類也實現了IActiveAware接口。CompositeCommand能夠經過擁有參數monitorCommandActivity的構造函數來配置是否評估子命令的活動狀態)。
若是monitorCommandActivity參數是true,CompositeCommand類會有如下行爲:安全

  • CanExecute。全部Active的命令均可以被執行時返回true。不活動的子命令不會被考慮。
  • Execute。運行全部的活動命令。不活動的子命令不會被考慮。

集合中綁定命令

另外一個場景的使用情景是, 在視圖中顯示一組數據的集合,同時須要爲每個數據項綁定一個命令,可是這個命令是在父視圖(視圖模型)中實現的(不是數據項類中實現的)。
好比,下圖的視圖使用ListBox顯示了一組數據,使用數據模板爲每個數據項顯示了一個Delete按鈕:
在集合中綁定命令
困難在於將視圖模型實現的Delete命令綁定到每個項。因爲每一項的數據上下文(ListBox中)引用的是集合中的項,而Delete命令在父View Model中實現。
一種解決方案是在數據模板中綁定命令。網絡

 
 
 
 
<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>

觸發器和命令的交互

使用Blend來設計觸發器交互:框架

 
 
 
 
<Button Content="Submit" IsEnable="{Binding CanSubmit}"> <i:Interaction.Triggers> <i:EventTrigger EventName="Click"> <i:InvokeCommandAction Command="{Binding SubmitCommand}"/> </i:EventTrigger> </i:Interaction.Triggers></Button>

這種方法能夠用於任何能夠附加交互觸發器的控件。若是想要將命令綁定到沒有實現ICommandSource接口的控件,或者想要調用自定義的事件來觸發命令時,這種方式尤爲有用。
下面代碼顯示了配置ListBox來監聽SelectionChanged事件。事件發生時會地調用綁定的命令:異步

 
 
 
 
<ListBox ItemsSource="{Binding Items}" SelectionModel="Single"> <i:Interaction.Triggers> <i:EventTrigger EventName="SelectionChanged"> <i:InvokeCommandAction Command="{Binding SelectedCommand}"/> </i:EventTrigger> </i:Interaction.Triggers></ListBox>

命令vs行爲async

爲命令傳入EventArgs參數

當想要調用命令來響應控件觸發的事件時,可使用Prism類庫中的InvokeCommandAction。Prism類庫的InvokeCommandAction與Blend SDK中的同名方法的區別以下:首先Prism類庫的InvokeCommandAction方法根據命令CanExecute方法的返回值更新控件的enable狀態。第二,若是沒有設置CommandParameter,Prism類庫的InvokeCommandAction方法能夠從父觸發器傳遞EventArgs參數(依賴項屬性TriggerParameterPath)。
有些狀況下,須要從父觸發器傳遞參數給命令,好比EventTrigger的EventArgs。這種狀況下不能使用Blend SDK中的InvokeCommandAction方法。代碼以下:異步編程

 
 
 
 
<ListBox Grid.Row="1" Margin="5" ItemsSource="{Binding Items}" SelectionModel="Single"> <i:Interaction.Triggers> <i:EventTrigger EventName="SelectionChanged"> <!-- 調用選擇命令,而且傳遞參數 --> <prism:InvokeCommandAction Command="{Binding SelectedCommand}" TriggerParameterPath="AddedItems"/> </i:EventTrigger> </i:Interaction.Triggers></ListBox>

處理異步交互

view Model常常面臨異步的交互,好比請求網絡服務和網絡資源,或者後臺的的計算或IO任務。使用異步能夠提供好的用戶體驗。
當用戶啓動了一個異步請求或後臺任務時,預測任務什麼時候完成很是困難。可是UI只能在UI線程中更新,因此須要頻繁的調度UI線程。函數

經過網絡服務獲取數據和進行交互

在異步編程模式中,須要調用一對方法而不是一個。爲了啓動異步調用,首先調用BeginXXX方法,當調用結束時調用EndXXX方法。
爲了決定調用EndXXX方法的時機,能夠選擇輪詢是否完成或者在調用BeginXXX方法時指定回調函數。回調方法以下:工具

 
 
 
 
IAsyncResult asyncResult = this.service.BeginGetQuestionnaire(GetQuestionnaireCompleted, null);private void GetQuestionnaireCompleted(IAsyncResult result){ try { questionnaire = this.service.EndGetQuestionnaire(ar); } catch(Exception ex) { //報錯 }}

注意,在End方法中,可能會遇到一些異常。須要處理這些異常,而且以線程安全的方式報告給UI。
因爲遠程響應通常都再也不UI線程,因此若是想要改變UI的狀態,必須將響應調度到UI線程(使用Dispatcher或者SynchronizationContext對象)。WPF中通常使用Dispatcher。
下面示例中,Questionnaire對象經過異步請求得到,而後將它設置爲QuestionnaireView的數據上下文。使用CheckAccess方法來判斷目前是否處於UI線程。

 
 
 
 
var dispatcher = System.Windows.Deployment.Current.Dispatcher;if(dispatcher.CheckAccess()){ QuestionnaireView.DataContext = questionnaire;}else{ dispatcher.BeginInvoke(()=>{ Questionnaire.DataContext = questionnaire; });}

用戶交互模式

有許多交互的方式,好比顯示對話框或MessageBox,可是在基於MVVM的應用中實現概念分離的交互是一個很大的挑戰。舉例來講,在非MVVM應用中經常使用的MessageBox,在MVVM應用中不能被使用,由於它會破壞view和view model概念之間的分離。
在MVVM模式中有兩種通用的方法來實現用戶交互。一種是實現一種View model使用的用戶交互服務,而且保持它和view的獨立性。另外一種方法是在view Model中經過觸發事件來向UI傳達意圖,這須要view中的組件綁定到這些事件。

使用交互服務

這種方法,view model一般依賴於交互服務接口。它會經過依賴注入容器或service Locator頻繁的請求交互服務。
一旦view Model得到了交互服務的引用,它就能在必要時請求交互服務。交互服務事項了交互的視覺效果,以下圖所示:
使用交互服務
模態交互,好比顯示一個MessageBox或彈出一個模態窗口,在運行繼續前須要一個指定的響應,因此能夠以同步的方式進行實現:

 
 
 
 
var result = interactionService.ShowMessageBox("Are you sure you want to cancel this operation?"), "Confirm", MessageBoxButton.OK);if(result == MessageBoxResult.Yes){ CancelRequest();}

這種方法的劣勢是強制了同步的編程模型。一個可選的異步實現是以下:

 
 
 
 
var result = interactionService.ShowMessageBox("Are you sure you want to cancel this operation?"), "Confirm", MessageBoxButton.OK,result =>{ if(result == MessageBoxResult.Yes)});

交互服務的異步實現更靈活一些。

使用交互請求對象

另外一個在MVVM模式中實現UI的方式是容許view model經過交互請求對象(與view中的行爲耦合)直接向view請求交互。交互請求對象封裝交互請求的細節和響應,而且經過事件來和view通訊。view通常將交互封裝在一個行爲中。
使用交互請求對象
Prism採用這種方式。Prism框架經過IInteractionRequest接口和InteractionRequest 類支持交互請求對象方式。IInteractionRequest接口定義額一個事件(Raise)來啓動交互。view中的行爲綁定到這個接口,並訂閱這個事件。InteractionRequest 類實現了IInteractionRequest接口,而且定義了兩個Raise方法來容許view Model啓動交互同時指定請求上下文,還能夠選擇傳遞一個回調函數。

從view Model初始化交互請求

上下文對象容許View Model傳遞數據和狀態給view。若是指定了回調,上下文對象還能夠回傳給View Model。

 
 
 
 
public interface IInteractionRequest{ event EventHandler<InteractionRequestionRequestedEventArgs> Raised;}public class InteractionRequest<T> : IInteractionRequest where T : INotification{ public event EventHandler<InteractionRequestedEventArgs> Raised; public void Raise(T context) { this.Raise(context, c => { }); } public void Raise(T context, Action<T> callback) { var handler = this.Raised; if(handler != null) { handler( this, new InteractionRequestedEventArgs( context, () => { if(callback != null) callback(context);} ) ); } }}

Prism提供了一些預約義的上下文類來支持通用的交互請求。INotification接口用來通知用戶發生了一個重要的事件。它提供了兩個屬性——Title和Content。一般通知都是單向的,因此不須要用戶在交互過程當中更改這些值。Notification類是該接口的默認實現。
IConfirmation接口擴展了INotification接口而且提供了第三個屬性——Confirmed,這個屬性用來標識用戶是確認仍是取消了操做。Confirmation類是該接口的默認實現,它實現了MessageBox風格的交互。能夠自定義上下文類來實現INotification接口封裝任何須要的數據和狀態。
View Model類會建立一個InteractionRequest 的實例,而且定義一個只讀的屬性(用來view綁定)。當View Model啓動交互請求時,會調用Raised方法,傳遞上下文對象,和可選的回調委託。

 
 
 
 
public InteractionRequestViewModel(){ this.ConfirmationRequest = new InteractionRequest<IConfirmation>(); ... //爲每一個按鈕定義一個命令,每個按鈕都引發不一樣的交互請求。 this.RaiseConfirmationCommand = new DelegateCommand(this.RaiseConfirmation); ...}public InteractionRequest<IConfirmation> ConfirmationRequest {get; private set;}private void RaiseConfirmation(){ this.ConfirmationRequest.Raise( new Confirmation{ Content = "Confirmation Message", Title = "Confirmation"}, c => { InteractionResultMessage = c.Confirmed ? "The user accepted." : "The user cancelled.";}); }}

Interactivity QuickStart示例展現瞭如何使用這些接口和類來完成交互。

使用行爲實現UI體驗

交互請求對象表示了邏輯的交互,實際的UI體驗被定義在view中。行爲常常被用來封裝UI體驗。UI設計師能夠將view Model中的交互請求對象綁定到行爲上。
View必須探測到一個交互請求事件,而後呈現請求的視覺效果。事件觸發器用來在探測到交互請求事件時進行初始化動做。
經過綁定到交互請求對象,Blend提供的標準EventTrigger能夠監視一個交互請求事件。然而,Prism定義了一個EventTrigger——InteractionRequestTrigger,能夠自動和IInteractionRequest接口的Raised事件進行鏈接。這減小了XAML的代碼量,同時減小了錯誤輸入事件名稱的可能。
事件觸發之後,InteractionRequestTrigger會調用指定的動做。Prism爲WPF提供了PopupWindowAction類,這個類能夠顯示一個彈出窗口。當窗口顯示時,它的數據上下文設置爲交互請求的上下文參數。使用PopupWindowAction的WindowContent屬性能夠指定彈出窗口的視圖。彈出窗口的標題被綁定到上下文對象的Title屬性。

注意:默認狀況下PopupWindowAction類顯示的窗口與上下文對象相關。若是是一個Notification上下文對象,DefaultNotificationWindow將會顯示,若是是Confirmation上下文對象,一個DefaultConfirmationWindow將會顯示。能夠經過WindowContent屬性來指定彈出窗口的視圖。

如何使用InteractionRequestTrigger和 PopupWindowAction:

 
 
 
 
<i:Interaction.Triggers> <prismInteractionRequestTrigger SourceObject="{Binding ConfirmationRequest, Mode=OneWay}"> <prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True"/> </prismInteractionRequestTrigger></i:Interaction.Triggers>

Prism的InteractionRequestTrigger和PopupWindowAction類能夠用做自定義觸發器和動做的基礎。

高級建立和裝配

使用MEF建立View和ViewModel

使用MEF,能夠經過使用Import特性來指定view的依賴,使用Export特性指定具體的View Model類型。
屬性設置View的數據上下文。能夠選擇使用屬性或者有參構造函數來爲View傳入View model。
好比,StockTrader RI的Shell view中聲明瞭一個只寫的屬性(ViewModel,Import特性標註)。視圖被實例化是,MEF穿件了指定View Model的實例,而且設置這個屬性值。代碼以下:

 
 
 
 
[Import]ShellViewModel ViewModel{ set { this.DataContext = value; }}

View Model定義以下:

 
 
 
 
[Export]public class ShellViewModel : BindableBase{ ...}

一個可選的方法是,在視圖中定義一個Importing Constructor。

 
 
 
 
public Shell(){ InitializeComponent();}[ImportingConstructor]public Shell(ShellViewModel viewModel) : this(){ this.DataContext = viewModel;}

MEF將會實例化一個ShellViewModel,並傳遞給Shell的構造器。

使用Unity建立View和ViewModel

使用Unity一樣有兩種依賴注入的方式。區別在於,使用Unity沒法在運行時被隱式地發現,它們必須被註冊到DI容器中。
一般,你須要爲view Model指定一個接口,這樣ViewModel的具體類型才能從view中解耦。

 
 
 
 
public Shell(){ InitializeComponent();}public Shell(ShellViewModel ViewModel):this(){ this.DataContext = viewModel;}

固然,也能夠定義一個只寫的屬性,Unity會實例化請求的View Model,而且調用屬性設置器來指定數據上下文。

 
 
 
 
public Shell(){ InitializeComponent();}[Dependency]public ShellViewModel ViewModel{ set { this.DataContext = value; }}

註冊到Unity容器的代碼以下:

 
 
 
 
IUnityContainer container;container.RegisterType<ShellViewModel>();

view 能夠經過容器來進行實例化:

 
 
 
 
IUnityContainer container;var view = container.Resolve<Shell>();

經過外部類來建立View和View Model

有些狀況下,可能須要定義一個controller或服務類來實例化這些view和View Model類。好比實現導航功能:

 
 
 
 
private void NavigateToQuestionnaireList(){ this.uiService.ShowView(ViewNames.QuestionnaireTemplatesList);}

ShowView經過容器建立一個視圖實例,而後顯示它:

 
 
 
 
public void ShowView(string viewName){ var view = this.ViewFactory.GetView(viewName);}

注意:Prism爲region的導航提供了支持。詳情見View-Based Navigation。

測試MVVM應用

測試Model和View Model與測試其它類使用相同的工具和技術——好比單元測試和模擬框架。可是,對於Model和View Model有一些典型的測試模式。

測試INotifyPropertyChanged接口的實現類

該接口的實現類容許視圖對模型和視圖模型的改變作出反應。這些改變並不限於控件中的領域數據,還有控制視圖的數據,好比視圖模型的狀態(控制動畫的開始或控件的enable狀態)。

簡單狀況

可以被測試代碼直接更新的屬性,能夠經過爲PropertyChanged事件設置一個事件處理函數來進行測試,在爲屬性設置一個新值,而後測試事件是否被觸發。有一些幫助類能夠用來附加事件處理函數而且收集測試結果,好比PropertyChangeTracker類。

 
 
 
 
var changeTracker = new PropertyChangeTracker(viewModel);viewModel.CurrentState = "newState";CollectionAssert.Contains(changeTracker.ChangedProperties, "CurrentState");

計算的和不可設定的屬性

若是屬性不能被測試代碼設置——好比屬性沒有公開的設置器或者是隻讀的,計算的屬性——測試代碼須要模擬對象來引發屬性的改變。

 
 
 
 
var changeTracker = new PropertyChangeTracker(viewModel);var question = viewModel.Questions.First() as OpenQuestionViewModel;question.Question.Response = "some text";CollectionAssert.Contains(changeTracker.ChangedProperties, "UnansweredQuestions");

整個對象

測試INotifyDataErrorInfo接口的實現類

實現綁定數據的輸入驗證有三種方式:在設置器中拋出異常、實現IDataErrorInfo接口、實現INotifyDataErrorInfo接口。第三種方式爲每一個屬性提供了多個錯誤報告的支持,同時意味着須要更多的測試。
INotifyDataErrorInfo接口有兩個方面須要測試:測試驗證規則的正確性和測試實現的接口,好比觸發ErrorsChanged事件。

測試驗證規則

驗證邏輯一般很容易進行測試,由於它是自包含的過程(輸出依賴於輸入)。對於每個有驗證規則的屬性,須要測試合法值,非法值,邊界值等等。

 
 
 
 
// 非法用例var notifyErrorInfo = (INotifyDataErrorInfo)question;question.Response = -15;Assert.IsTrue(notifyErrorInfo.GetErrors("Response").Cast<ValidationResult>().Any());// 合法用例var notifyErrorInfo = (INotifyDataErrorInfo)question;question.Response = 15;Assert.IsFalse(notifyErrorInfo.GetErrors("Response").Cast<ValidationResult>().Any());

跨屬性的驗證規則遵循相同的測試模式,通常須要更多的測試來組合不一樣的屬性值。

測試INotifyDataErrorInof接口的實現

實現INotifyDataErrorInfo接口必須確保ErrorsChanged事件在適當的時候被觸發、HasErrors屬性必須反應對象的整個錯誤狀態。
並非全部被驗證的屬性都必須被測試。
測試接口的須要至少包括:

  • HasErrors屬性反映對象的全局錯誤狀態。爲原先非法的屬性設置一個合法值,其它的屬性值保持非法,判斷該屬性的結果是否改變。
  • ErrorsChanged事件被觸發(當一個屬性的錯誤狀態改變時,經過GetErrors方法的結果反映)。錯誤狀態從一個合法狀態到非法狀態,還有反過來,還能夠從一個非法狀態到另外一個非法狀態,若是GetErrors的結果發生改變,說明ErrorsChanged事件被觸發了。

 
 
 
 
var helper = new NotifyDataErrorInfoTestHelper<NumericQuestion, int?>(question, q => q.Response);helper.ValidatePropertyChange( 6, NotifyDataErrorInfoBehavior.FiresErrorsChanged | NotifyDataErrorInfoBehavior.HasErrors | NotifyDataErrorInfoBehavior.HasErrorsForProperty);helper.ValidatePropertyChange( null, NotifyDataErrorInfoBehavior.FiresErrorsChanged | NotifyDataErrorInfoBehavior.HasErrors | NotifyDataErrorInfoBehavior.HasErrorsForProperty);helper.ValidatePropertyChange( 2, NotifyDataErrorInfoBehavior.FiresErrorsChanged);

測試異步服務調用

雖然基於事件的異步設計模式(Event-based Asynchronous design pattern)能確保事件在合適的線程上調用,可是IAsyncResult設計模式不能提供任何線程安全的保證。
處理線程關注點很複雜,所以一般也很難編寫測試代碼。一般要求測試代碼自己也是異步的。當通知確實在UI線程發生時,不是由於使用了標準的基於事件的異步設計模式,仍是由於視圖模型依賴於一個服務訪問層(分發通知到合適的線程),測試本質上都扮演了UI線程調度(Dispatch for the UI thread)的角色。
模擬服務的方式依賴於實現服務操做的異步事件模式。若是使用的是基於方法的模式(method-based based pattern),用標準的mock框架來模擬一個服務接口就足夠了;可是若是使用基於事件的模式(event-Based pattern),首選的方案是模擬一個定製的類(實現增長,移除服務處理事件的方法)。
下面的例子顯示了經過模擬服務,在完成異步操做後通知UI線程進行適當行爲的一個測試。這個示例中,測試代碼獲取View Model爲異步服務調用提供的回調,而後經過調用這個回調來模擬異步服務調用完成。這種方法使測試一個組件的異步服務而無需編寫複雜的異步測試。

 
 
 
 
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);ComplateQuestionnaire(viewModel);viewModel.Submit();//模擬callback(submitResultMock.Object);//測試行爲——請求導航到list 視圖Assert.AreEqual(ViewNames.QuestionnaireTemplatesList, requestedViewName);

注意:使用這種測試方法僅僅能保證覆蓋功能測試,並不能測試代碼是不是線程安全的。

擴展閱讀



相關文章
相關標籤/搜索