Prism初研究之使用Prism實現WPF的MVVM模式


Prism初研究之使用Prism實現WPF的MVVM模式

MVVM模式幫助你清晰地將業務邏輯和UI邏輯分離開。維持清晰的UI和業務邏輯的分離有助於專注於分發開發和設計,並且使你的應用更容易測試、維護和迭代。MVVM還有改善代碼重用,允許開發者和UI設計者容易合作等優點。
使用MVVM模式,將UI和展示邏輯和業務邏輯分成三類:視圖(封裝UI和UI邏輯),視圖模型(封裝展示邏輯和狀態),模型(封裝應用的業務邏輯和數據)。
Prism提供了一些事項MVVM的WPF應用示例。Prism提供了一些特性來幫助使用者在自己的應用中實現MVVM。這些特性包括MVVM的大多數通用的實踐,並且可測試,在Blend和Visual Stdio中工作良好。
本章提供MVVM模式的綜述,並且講述如何實現該模式的基本特征。

類職責和特征

MVVM模式是從PM(演示模式)變化來的,它充分利用了WPF的核心優勢,比如數據綁定、數據模板、命令和行為。
MVVM模式中,視圖封裝UI和UI邏輯,視圖模型封裝演示邏輯和狀態,模型封裝業務邏輯和數據。視圖與視圖模型通過數據綁定、命令,更改通知事件進行交互。視圖模型查詢、觀察並且協助模型的更新,同時為視圖層轉換、驗證、收集需要顯示的數據。
下圖展現了MVVM類和它們之間的交互:
MVVM類和交互
和其它所有分開展現的模式一樣,使用MVVM模式的關鍵在於將應用程序的代碼放入正確的類中,並且深入理解各種情景中類之間的交互。下面的章節描述MVVM中每個類的職責和特征。

視圖類(View)

視圖類的職責是定義用戶在屏幕上所看到的結構和外貌。理想的視圖類的構造函數只調用InitializeComponent方法。有些情況下,視圖類的后台代碼包含了UI的邏輯,因為使用XAML來實現這些顯示的行為可能非常困難或者效率低下,比如復雜的動畫,或是代碼需要直接操作視圖中的顯示元素。如果需要進行單元測試的話,不應該在視圖類中放入任何邏輯代碼。一般視圖后台代碼中的邏輯會通過UI自動化測試方法進行測試。
WPF中視圖中數據綁定表達式與它的數據上下文有關。MVVM中,視圖的數據上下文設置為視圖模型。視圖模型實現視圖需要綁定的屬性和命令,並且通過通知事件來通知視圖任何狀態的改變。視圖和視圖模型通常情況下是一對一的關系。
視圖一般情況下是Control和UserControl類的派生類。但是,有些情況下,視圖可能通過數據模板來展現。視覺設計師使用數據模板可以很容易地定義視圖模型的渲染方式,或者在無需改變對象本身和展示對象的控件行為的情況下改變視圖默認的視覺展現。
數據模板可以認為是沒有任何后台代碼的視圖。它們會根據指定的視圖模型類型來進行設計。在運行時,使用數據模板定義的視圖將會被動態實例化,它的數據上下文會設置為相關聯的視圖模型。
在WPF中,可以在應用程序級將數據模板和視圖模型類型對應起來。當視圖需要顯示時,WPF會自動為數據模板提交指定的視圖模型對象。這種方式是隱式的數據模板。數據模板可以內聯在空間中,也可以定義在視圖外的資源字典中(在視圖的資源字典中要聲明合並)。
總的來說,視圖有以下關鍵的特征:

  • 視圖是一個顯示元素,比如,窗口、頁、控件或是數據模板。視圖定義了包含的控件、布局和樣式。
  • 視圖通過DataContext來引用視圖模型。視圖中的控件通過綁定視圖模型暴露的數據和命令來進行交互。
  • 視圖可以自定義數據綁定的行為。比如,視圖可以使用數據轉換器(value converters)來格式化UI要顯示的數據,它也可以使用驗證規則來提交正確的數據。
  • 可以在視圖的后台代碼中定義UI邏輯來實現視覺行為,因為使用XAML可能非常困難或者,需要直接引用定義在視圖中UI控件。

視圖模型類(View Model)

MVVM模式中的視圖模型類為視圖封裝了數據和展示邏輯。它無需直接引用視圖,也無需知道視圖類的特定實現和類型。視圖模型實現用於數據綁定和狀態更改通知的屬性和命令。雖然UI使用的屬性和命令都定義在視圖模型中,但是視圖決定這些功能如何進行渲染。
視圖模型類負責視圖和模型類的交互。通常視圖模型和模型是一對多的關系。視圖模型可能將模型類直接暴露給視圖,這樣視圖就可以直接綁定到模型了。這種情況下,模型類就需要設計支持數據綁定和更改通知。詳情見數據綁定。
視圖模型可以轉換和修改模型數據,這樣一來模型數據很容易用來填充視圖。視圖模型可以特意為視圖增加額外的屬性;這樣屬性正常來說不屬於模型(不能在模型中添加)。比如,視圖模型可能聯合了兩個字段的值類方便視圖的展示,或者視圖模型可能計算還可以輸入的字符數。視圖模型也可能實現數據的驗證邏輯來確保數據的一致性。
視圖模型還可能定義UI視圖改變的狀態邏輯。視圖可以根據視圖模型的狀態改變布局和樣式的定義。比如,視圖模型可以定義一個狀態來表示數據是通過異步的方式提交到網絡的。在這個狀態過程中,視圖可以顯示一個動畫來向用戶進行反饋。
通常視圖模型會定義UI和用戶調用的命令和動作。一個普通的例子:視圖模型提供一個Submit命令來運行用戶向網絡或數據庫提交數據。視圖可能選擇將命令綁定到一個按鈕來允許用戶通過點擊按鈕提交數據。一般情況下,如果命令不能夠被使用,和它相關的UI元素也應該變成disable。Commands提供了一種分離視圖和用戶動作的方法。
總的來說,視圖模型的關鍵特征如下:

  • 視圖模型不是一個可視化的類,它不會從WPF的任何類派生。它封裝的展現邏輯要求支持一個用例或用戶任務。視圖模型可以獨立於視圖和模型進行單獨測試。
  • 通常,視圖模型不會直接引用視圖。它實現視圖需要綁定的屬性和命令。它通過INotifyPropertyChanged和INotifyCollectionChanged接口來通知視圖,任何狀態發生了改變。
  • 視圖模型協助視圖和模型間的交互。它可以轉換和修改數據以便於視圖使用,也可以實現模型中沒有提供的額外屬性,也可以通過IDataErrorInfo或INotifyDataErrorInfo接口來實現數據驗證。、
  • 視圖模型可以定義邏輯狀態,視圖可以根據狀態展現不同的視覺效果。

    View還是View Model?
    很多時候,在何處實現何工能並不是顯而易見的。一般的規則是:屏幕上與特定的UI可視元素相關的和后期可能重新設計樣式的功能應該在View中實現;那些對應用程序來說很重要的邏輯行為應該放在View Model中實現。因為View Model並不了解視圖中指定的可視化元素,所以可視化元素的操作代碼應該放在View的后台代碼中,或者封裝在一個行為中。相似的,檢索和操作通過數據綁定顯示在View中的數據項應該在View Model中編碼。
    比如,listbox中選中的項應該在View中定義,但是顯示的項目和選中數據項的引用應該在ViewModel中定義。

模型類(Model)

MVVM模式中的模型類封裝業務邏輯和數據。業務邏輯是應用程序中與檢索、管理應用數據,確保數據的一致性和正確性,滿足業務規則相關的所有應用邏輯。為了最大化復用的優勢,模型不應該包含任何用例、用戶指定工作行為、應用程序邏輯。
通常,模型表示應用程序的領域模型。它可以根據應用程序數據模型和支持的業務和驗證邏輯定義數據結構。模型類也可以包括致辭數據訪問和緩存的代碼,雖然一個分離的數據存儲和訪問服務更常見。

類間的交互

MVVM模式將UI,展現邏輯,業務邏輯和數據隔離在分開的類中。因此,將代碼放入正確的類中是一個很重要的因素。
設計良好的視圖,視圖模型,模型類不僅僅封裝了正確的類型和行為,還很容易通過數據綁定、命令和數據驗證接口來進行交互。
視圖和視圖模型間的交互可能是最需要仔細考慮的,當然模型類和視圖模型間的交互也很重要。下面的小節將描述交互的各種模式,還有如何設計它們。

數據綁定(Data Binding)

數據綁定在MVVM中扮演非常重要的角色。WPF提供了非常強大的數據綁定能力。視圖模型類和模型類(理想的)應該支持數據綁定來利用這些能力的優勢。這意味着它們必須實現正確的接口。
WPF的數據綁定支持多種數據綁定模式。One-way模式:UI控件在顯示時渲染View Model中的數據;Two-way模式:當用戶在UI中改變數據時,后台的數據會自動更新。
為了確保View Model中數據改變時UI能保持一致,應該實現更改通知接口。視圖模型中的數據屬性,應該實現INotifyPropertyChange接口;視圖模型中的集合應該實現INotifyCollectionChanged接口,或者從ObservableCollection 類(實現了INotifyCollectionChanged接口)派生。這兩個接口都定義了一個事件,當數據更改時這個事件將被觸發。綁定到該數據的控件會在事件觸發時自動更新。
很多情況下,一個視圖模型定義的屬性可能返回對象(也定義了屬性,並返回其它對象)。WPF的數據綁定通過Path屬性支持綁定到嵌套的屬性。因此,一個視圖的視圖模型返回另一個視圖模型或者模型類的引用很常見。視圖可以訪問的所有的視圖模型類和模型類都應該實現INotifyCollectionChanged或INotifyPropertyChanged接口。

實現INotifyPropertyChanged

實現這個接口很簡單:

 
 
 
         
  1. public class Questionnaire : INotifyPropertyChanged
  2. {
  3. private string favoriteColor;
  4. public event PropertyChangedEventHandler PropertyChanged;
  5. ...
  6. public string FavoriteColor
  7. {
  8. get { return this.favoriteColor;}
  9. set
  10. {
  11. if(value != this.favoriteColor)
  12. {
  13. this.favoriteColor = value;
  14. var handler = this.PropertyChanged;
  15. if(handler != null)
  16. {
  17. handler(this, new PropertyChangedEventArgs("FavoriteColor"));
  18. }
  19. }
  20. }
  21. }
  22. }

在許多視圖模型類中實現INotifyPropertyChanged接口需要重復很多次,並且很容易出錯(需要傳遞屬性名稱)。Prism類庫中提供BindableBase基類,這個類以一種類型安全的方式實現了INotifyPropertyChanged接口,View Model類可以從該類派生。

 
 
 
         
  1. public abstract class BindableBase : INotifyPropertyChanged
  2. {
  3. public event PropertyChangedEventHandler PropertyChanged;
  4. ...
  5. propected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
  6. {
  7. if (object.Equals(storage, value)) return false;
  8. storage = value;
  9. this.OnPropertyChanged(propertyName);
  10. return true;
  11. }
  12. propected void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
  13. {
  14. var eventHandler = this.PropertyChanged;
  15. if (eventHandler != null)
  16. {
  17. eventHandler(this, new PropertyChangedEventArgs(propertyName));
  18. }
  19. }
  20. propected void OnPropertyChanged(string propertyName)
  21. {
  22. var propertyName = PropertySupport.ExtractPropertyName(propertyExpression);
  23. this.OnPropertyChanged(propertyName);
  24. }
  25. }

一個派生的視圖模型類可以在屬性的設值處調用SetProperty方法。該方法會檢查字段是否與將要設值的值相同,如果不同,字段會被更新,PropertyChanged事件會觸發。
下面的代碼顯示了如何設值屬性,同時使用lambda表達式(調用OnpropertyChanged方法)來改變另一個屬性。

 
 
 
         
  1. public TransactionInfo TransactionInfo
  2. {
  3. get{ return this.transactionInfo; }
  4. set
  5. {
  6. SetProperty(ref this.transactionInfo, value);
  7. this.OnPropertyChanged(() => this.TickerSymbol);
  8. }
  9. }

注意:使用Lambda表達式會帶來一點性能消耗。這種方法的優點是提供了編譯時的類型安全和反射支持(如果要重命名屬性)。雖然性能消耗很小,一般不會影響應用程序,但是如果有很多更改通知的話,影響會比較明顯。這種情況下,需要考慮是否放棄使用Lambda表達式。

模型和視圖模型經常包含其它屬性計算值的屬性。處理這種屬性的變化時,確保觸發了所有參與計算的屬性的更改通知事件。

實現INotifyCollectionChanged

視圖模型或模型類可能表示一組數據項,或者可能定義了一個或多個返回數據項集合的屬性。不過是哪種情況,你都需要在一個ItemsControl中顯示它們,比如ListBox、DataGrid。這些控件將ItemsSource綁定到表示集合的視圖模型或返回一個集合的屬性。

 
 
 
         
  1. <DataGrid ItemsSource="{Binding Path=LineItems}"/>

為了實現更改通知,表示集合的視圖模型或模型應該實現INotifyCollectionChanged接口。如果視圖模型或模型定義了一個返回集合的屬性,那么返回的集合類應該實現INotifyCollectionChanged接口。
然而,實現INotifyCollectionChanged接口是一個不小的挑戰,因為不管是增加、移除還是改變集合中的項都應該提供更改通知。除了直接實現INotifyCollectionChanged接口,更簡答的方法是使用或者繼承一個已經實現該接口的集合類。ObservableCollection 類提供了這個接口的實現,這個類經常作為表示集合的屬性或者基類。
如果你需要為視圖的數據綁定提供一個集合,並且不需要跟蹤用戶的選擇、過濾、排序和分組,那么可以簡單的在視圖模型中定義一個返回ObservableCollection 實例引用的屬性。

 
 
 
         
  1. public class OrderViewModel : BindableBase
  2. {
  3. public OrderViewModel( IOrderService orderService )
  4. {
  5. this.LineItems = new ObservableCollection<OrderLineItem>(orderService.GetLineItemList());
  6. public ObservableCollection<OrderLineItem> LineItems { get; private set; }
  7. }
  8. }

如果需要獲得了一個集合類的引用(比如,從其它沒有實現INotifyCollectionChanged接口的組件和服務),那么應該使用一個接收IEnumerable 或者List 參數的構造函數將這個集合包裝成ObservableCollection

注意:BindableBase在Microsoft.Practices.Prism.Mvvm命名空間(在Prism.Mvvm包中)。

實現ICollectionView

視圖中集合項的顯示通常需要更細致的控制,在View Model中跟蹤顯示的集合項的用戶交互。比如,你可能需要集合項能夠根據View Model中的展現邏輯進行過濾或者排序,還可能需要跟蹤視圖中當前被選中的項(這樣View Model中實現的Commands能夠作用到選中的項)。
WPF提供一些實現ICollectionView接口的類來支持這些應用場景。這個接口提供了允許集合過濾、排序、分組、以及選中項的跟蹤和更改的屬性和方法。ListCollectionView是WPF提供的一個該接口的實現。
集合視圖類包裝了一個數據項集合,所以可以提供動態的選擇跟蹤、排序、過濾以及分頁。這些類的實例可以編程創建,也可以使用CollectionViewSource類在XAML中聲明。

注意:在WPF中,一旦控件綁定到了一個集合,默認的集合視圖就會自動被創建。

如果需要在View Model中實現過濾、排序、分組和選擇跟蹤,那么就需要為暴露給View的每一個集合都創建一個Collection View類的實例。然后可以訂閱選擇改變事件(比如CurrentChanged事件),使用View Model中實現的控制過濾、排序、分組的方法。
View Model應該實現一個返回ICollectionView的readonly屬性。所有派生子ItemsControl的WPF空間都可以自動和ICollectionView類交互。
使用ListCollectionView跟蹤選中的customer:

 
 
 
         
  1. public class MyViewModel : BindableBase
  2. {
  3. public ICollectionView Customers { get; private set; }
  4. public MyViewModel( ObservableCollection<Customer> customers )
  5. {
  6. Customers = new ListCollectionView( customers );
  7. Customers.CurrentChanged += SelectedItemChanged;
  8. }
  9. private void SelectedItemChanged( object sender, EventArgs e )
  10. {
  11. Customer current = Customers.CurrentItem as Customer;
  12. ...
  13. }
  14. ...
  15. }

將Customers屬性綁定到ItemsControl(比如ListBox):

 
 
 
         
  1. <ListBox ItemsSource="{Binding Path=Customers}">
  2. <ListBox.ItemTemplate>
  3. <DataTemplate>
  4. <StackPanel>
  5. <TextBlock Text="{Binding Path=Name}"/>
  6. </StackPanel>
  7. </DataTemplate>
  8. </ListBox.ItemTemplate>
  9. </ListBox>

當用戶從UI選擇了一個customer時,View Model將得到通知調用與當前選擇customer相關的命令。View Model也可以編程調用collection view中的方法來改變UI中選中的項:

 
 
 
         
  1. Customers.MoveCurrentToNest();

當collection view中的選擇發生變化時,UI會自動更新為展現選中項的狀態。

命令(Commands)

WPF中,用戶通過UI進行的動作和操作通常都定義成命令。命令可以很容易的綁定到UI控件。命令封裝了動作或操作的實現代碼,並且將它們從視圖中解耦出來。
用戶有許多方式可以調用命令。大多數情況下,它們通過鼠標點擊調用,但是它們也可以響應快捷鍵,觸摸控制、或者其它輸入事件。UI控件可以和命令進行雙向交互,命令可以被UI控件調用,UI控件也可以根據命令狀態自動設置為enable和disable。
View Model能夠使用一個Command Method或者一個Command對象(實現ICommand接口)來實現命令。兩種方式都無需視圖的后台代碼編寫復雜的事件處理函數。比如:
WPF中的某些控件提供了一個Command屬性用於綁定到視圖模型的ICommand對象。

實現Task-Based DelegateCommand

在很多情況下,命名的處理代碼需要花費很長的時間,但是又不能阻塞UI線程,此時,應該使用DelegateCommand類的FromAsyncHandler方法(使用異步的處理方法創建了一個DelegateCommand的新實例)。

 
 
 
         
  1. // DelegateCommand.cs
  2. public static DelegateCommand FromAsyncHandler( Func<Task> exeecuteMethod, Func<bool> canExecuteMethod)
  3. {
  4. return new DelegateCommand(executeMethod, canExecuteMethod);
  5. }

下面代碼表示如何視圖模型的SignInAsync 和CanSignIn方法來構造DelegateCommand實例。

 
 
 
         
  1. //SignInFlayoutViewModel.cs
  2. public DelegateCommand SignInCommand { get; private set; }
  3. ...
  4. SignInCommand = DelegateCommand.FromAsyncHandler(SignInAsync, CanSignIn);

實現命令對象

命令對象是實現了ICommand接口的對象。該接口包括Execute(封裝操作)、CanExecute(控制命令在合適的時間被調用)。
雖然實現ICommand接口很簡單,但是還是提供了一些該接口的實現。比如ActionCommand(Blend for Visual Studio SDK)、DelegateCommand(Prism)。

注意:DelegateCommand 在Microsoft.Practices.Prism.Mvvm命名空間(Prism.Mvvm包中)。

DelegateCommand封裝了兩個委托(在View Model類中實現)。它繼承自DelegateCommandBase基類(通過調用這兩個委托實現了ICommand接口)。可以在View Model類中通過DelegateCommand的構造函數指定委托:

 
 
 
         
  1. public class DelegateCommand<T> : DelegateCommandBase
  2. {
  3. public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecutedMethod ): base((o) => executedMethod((T)o), (o) => canExecutedMethod((T)o))
  4. {
  5. ...
  6. }
  7. }

下面的例子展示了如何使用QuestionnaireViewModel中的OnSubmit和CanSubmit方法構造Submit命令:

 
 
 
         
  1. public class QuestionnaireViewModel
  2. {
  3. public QUestionnaireViewModel()
  4. {
  5. this.SubmitCommand = new DelegateCommand<object>(this.OnSubmit, this.CanSubmit );
  6. }
  7. public ICommand SubmitCommand{ get; private set; }
  8. private void OnSubmit(object arg) {...}
  9. private bool CanSubmit(object arg) { return true; }
  10. }

構造函數中CanExecute方法的委托是可選的,如果沒有指定,DelegateCommand的CanExecute默認返回true。
上例中,DelegateCommand 通過指定object指定了Execute和CanExecute方法的參數是object。Prism還提供了一個無需命令參數版本的DelegateCommand。
View Model可以通過調用DelegateCommand的RaiseCanExecuteChanged方法來觸發CanExecuteChanged事件。UI中所用綁定這個命令的控件都會更新它們的enable狀態。
ICommand還有一些其它的實現。ActionCommand類和DelegateCommand類相似,區別在於它只支持一個Execute方法委托。Prism還提供了CompositeCommand類,這個類允許將一些DelegateCommand分組運行。

在視圖中調用命令

有許多將命令與控件關聯的方法。最常見的是繼承自ButtonBase的控件,比如Button、RadioButton,還有繼承自HyperLink,MenuItem的控件。 它們都可以通過Command屬性來綁定到視圖模型的ICommand。WPF還支持將View Model的ICommand綁定到KeyGesture。

 
 
 
         
  1. <Button Command="{Binding Path=SubmitCommand}" CommandParameter="SubmitOrder"/>

Execute和CanExecute的參數可以通過CommandParameter屬性來進行綁定(可選)。
還有一個可供選擇的方案是使用Blend來完成觸發器(Trigger)和InvokeCommandAction行為的交互。

數據驗證和錯誤報告

View Model和Model經常要求對數據進行驗證,並在View中顯示驗證錯誤,以便用戶修改數據。
WPF對數據驗證的錯誤提供了管理。對於綁定到控件的單一屬性,視圖模型或模型可以通過在屬性設置中拋出一個異常。如果數據綁定中的ValidatesOnException屬性被設置為true,那么WPF的數據綁定引擎將會在視圖上顯示一個數據驗證錯誤。
但是拋出屬性異常的方式在某些情況下會被忽略。替代方案是在View Model或Model類實現IDataErrorInfo或者INotifyDataErrorInfo接口。這兩個接口允許View Model和Model類完成一個或多個屬性的數據驗證,並且返回給View一個錯誤消息。

實現IDataErrorInfo

IDataErrorInfo接口為屬性的數據驗證和錯誤報告提供了基本的支持。它定義了兩個只讀的屬性:一個索引器(Item,屬性名是索引),一個Error屬性,兩個屬性都返回字符串。
Item屬性允許View model或者model為命名的屬性提供一個錯誤消息。如果屬性值是合法的,錯誤消息會是一個空字符串或者null。Error屬性為view Model和model整個對象提供一個錯誤消息。
Item屬性在數據第一次顯示和發生改變時被訪問。由於所有的屬性在發生改變時都會訪問Item屬性,所以應該
確保數據驗證盡可能的快速進行。
視圖中控件數據綁定的ValidatesOnDataErrors屬性應該設置為true。這樣數據綁定引擎就會請求錯誤信息。

 
 
 
         
  1. <TextBox Text="{Binding Path=CurrentEmployee.Name, Mode=TwoWay, ValidatesOnDataErrors=True, NotifyOnValidationError=True }"/>

實現INotifyDataErrorInfo

INotifyDataErrorInfo接口比IDataErrorInfo接口更靈活。它支持一個屬性的多個錯誤,異步的數據驗證,以及在對象錯誤狀態的改變時通知視圖的能力。
INotifyDataErrorInfo接口定義了一個HasErrors屬性(允許視圖模型表示是否存在錯誤)和GetErrors方法(允許視圖模型返回指定屬性的錯誤列表)。
INotifyDataErrorInfo接口還定義了ErrorsChanged事件。視圖和視圖模型通過這個事件來實現異步的數據驗證。除了數據綁定之外,屬性值還可以通過其它方法改變。比如網絡服務調用,或者后台計算。一旦數據驗證錯誤被識別,View Model通過ErrorsChanged來通知視圖。
為了支持INotifyDataErrorInfo,需要為每個屬性定義一個錯誤列表。

 
 
 
         
  1. // DomainObject: 模型中的根對象
  2. public abstract class DomainObject : INotifyPropertyChangedINotifyDataErrorInfo
  3. {
  4. private ErrorsContainer<ValidationResult> errorsContainer = new ErrorsContainer<ValidationResult>(pn => this.RaiseErrorsChanged( pn ));
  5. public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
  6. public bool HasErrors
  7. {
  8. get{ return this.ErrorsContainer.HasErrors; }
  9. }
  10. public IEnumerable GetErrors( string propertyName )
  11. {
  12. return this.errorsContainer.GetErrors( propertyName );
  13. }
  14. protected void RaiseErrorsChanged( string propertyName )
  15. {
  16. var handler = this.ErrorsChanged;
  17. if(handler != null)
  18. {
  19. handler(this, new DataErrorsChangedEventArgs(propertyName));
  20. }
  21. }
  22. ...
  23. }

創建View Model

使用XAML創建View Model

這是最簡單的創建View Model的方式。代碼如下:

 
 
 
         
  1. <UserControl.DataContext>
  2. <my:MyViewModel/>
  3. </UserControl.DataContext>

當View創建時,MyViewModel自動創建,並且設置為View的data context。該方法要求View Model有一個默認的構造函數。
該方式的優點是簡單,並且在設計時工具(Blend,或者Visual Studio)上工作的很好。缺點是需要知道View Model的類型,並且需要View Model擁有默認的構造函數。

編程創建View Model

在View的構造函數中創建View Model實例,並且設置DataContext。

 
 
 
         
  1. public MyView()
  2. {
  3. InitializeComponent();
  4. this.DataContext = new MyViewModel();
  5. }

優缺點和使用XAML一樣,區別是可以使用依賴注入容器來保持View和View Model的松耦合。

使用View Model Locator創建View Model

Prism中的View Model Locator有一個AutoWireViewModel附加屬性。當這個屬性設置為true時,ViewModelLocator會為這個View尋找View Model,然后將Datacontext設置為View Model的一個實例。

Basic MVVM QuickStart例子中MainWindow.xaml使用View Model Locator來創建View Model。

 
 
 
         
  1. <Window
  2. ...
  3. prism:ViewModelLocator.AutoWireViewModel="True">

ViewModelLocationProvider首先嘗試從映射關系(通過ViewModelLocationProvider類的Register方法注冊)中查找View Model,如果沒有找到,就用使用約定的方法來查找View Model的類型。約定是,View Model和View在同一個程序集中,而且ViewModel在“.ViewModels”子命名空間,View在“.Views”子命名空間,並且ViewModel的名字和View的名字匹配並以“ViewModel.”結尾。

創建數據模板

View可以被定義為一個數據模板。數據模板可以定義為資源;或者內聯在控件中。控件的內容是View Model實例,數據模板用於展示它。WPF在運行時會自動實例化數據模板並且將數據模板的上下文設置為view Model的實例。
數據模板既靈活又輕量。UI設計師無需編寫復雜的代碼就可以定義View Model的展現。一般使用Blend來設計編輯數據模板。

 
 
 
         
  1. <ItemsControl ItemsSource="{Binding Customers}">
  2. <ItemsControl.ItemTemplate>
  3. <DataTemplate>
  4. <StackPanel Orientation="Horizontal">
  5. <TextBlock VerticalAlignment="Center" Text="Customer Name: " />
  6. <TextBox Text="{Binding Name}" />
  7. </StackPanel>
  8. </DataTemplate>
  9. </ItemsControl.ItemTemplate>
  10. </ItemsControl>

也可以將數據模板定義為一個資源:

 
 
 
         
  1. <UserControl ...>
  2. <UserControl.Resources>
  3. <DataTemplate x:Key="CustomerViewTemplate">
  4. <local:CustomerContactView/>
  5. </DataTemplate>
  6. </UserControl.Resources>
  7. <Grid>
  8. <ContentControl Content="{Binding Customer}" ContentTemplate="{StaticResource CustomerViewTemplate}"/>
  9. </Grid>
  10. </UserControl>

當然,數據模板也可以放在應用程序的資源中。

關鍵決定

在選擇使用MVVM模式構建應用程序之前,需要做出一些設計決策(項目后期很難改變)。一般在項目開發設計過程中始終堅持這些約定,能讓開發和設計更有生產力。下面總結了一些最重要的選擇:

  1. 決定View和View Model的構建方式。需要決定應用是否首先構造views或者view models,還是選用一個類似Unity或MEF的DI容器。你應該將這個選擇廣泛應用在程序開發中。
  2. 決定是否view models中的commands是通過命令方法還是命令對象的方式暴露給views。Command可以在視圖中通過行為調用,這非常簡單。方法對象則封裝了命令和enable/disable邏輯,它能夠通過行為和繼承自ButtonBase控件的Command屬性調用。
    為了簡化開發和設計工作,最好在應用中遵守一致的選擇。
  3. 決定view Model和model提交驗證錯誤的方式。可以選擇實現IDataErrorInfo接口還是INotifyDataErrorInfo接口。
  4. 決定是否需要設計時數據支持(Blend)。如果選用Blend開發,確保view和View model提供無參數的構造函數。

擴展閱讀






免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM