使用MVVM設計模式構建WPF應用程序

使用MVVM設計模式構建WPF應用程序html

本文是翻譯大牛Josh Smith的文章,WPF Apps With The Model-View-ViewModel Design Pattern,譯者水平有限,若有什麼問題請看原文,或者與譯者討論(很是樂意與你討論)。數據庫


本文討論的內容:編程

WPF與設計模式、MVP模式、對WPF來講爲何MVVM是更好的選擇、用MVVM構建WPF程序、本文涉及的技術、WPF、數據綁定。設計模式

目錄:多線程

1引言架構

2有序與混亂app

3模型-視圖-視圖模型的演變框架

4爲何WPF開發者喜歡MVVMide

5演示程序函數

  5.1中繼命令邏輯

  5.2ViewModel類層級結構

  5.3ViewModelBase類

  5.4CommandViewModel類

  5.5MainWindowViewModel類

  5.6View對應ViewModel

  5.7數據模型和Repository

  5.8新增客戶數據表單

  5.9全部客戶視圖

6總結


1引言

開發UI,對一個專業軟件並不容易。它須要未知數據、交互式設計,可視化設計、聯通性,多線程、國際化、驗證、單元測試以及其餘的一些東西才能完成。考慮到UI要展現開發的系統而且必須知足用戶對系統風格不可預知的變動,所以它是不少應用程序最脆弱的地方。

有不少的設計模式能夠幫助解決UI不斷變動這頭難纏的野獸,可是恰當的分離和描述多個關注點可能很困難。模式越複雜,以後用到的捷徑越可能破壞以前正確的努力。

這並不老是設計模式的錯。有時使用要寫不少的代碼複雜設計模式,這是由於咱們使用的UI平臺並不適合簡單是設計模式。UI平臺須要作的是很容易使用簡單的,久經考驗的,開發者認識的設計模式構建UI。慶幸的是,WPF就是這樣一個平臺。

隨着是使用WPF開發的比例不斷升高,WPF社區發展了本身的模式與實踐生態圈子。在本文,我將討論一些設計與實現客戶端應用程序的WPF最佳實踐。利用WPF和MVVM設計模式銜接的一些核心功能,我將經過一個例子介紹,用「正確」的方式構建一個WPF程序是多麼的簡單。

data templates, commands, data binding, the resource system以及 MVVM 模式怎麼揉合到一塊兒建立一個簡單的、可測試的、健壯的框架,而且任何WPF程序都能使用,到文章最後,這一切都很清晰明瞭。文中的例程能夠做爲現實中一個WPF應用程序的模版,而且使用MVVM設計模式做爲其核心架構。例程解決方案中的單元測試部分,展現了測試ViewModel類的功能是很容易的。在深刻本文以前,咱們首先看一下咱們要使用像MVVM這樣的設計模式。

2有序與混亂

沒有必要在一個」Hello,World!」的程序中使用設計模式。任何一個合格的開發者看一眼就指導那幾行代碼是幹什麼的。然而隨着程序功能點的增長,隨之代碼的數量以及移動部件也會增多。最終系統的複雜度以及不斷出現問題,促使開發者組織他們的代碼,以便它們更容易理解,討論、擴展以及維護。咱們經過給代碼中某些實體命以衆所周知的名字,減小複雜系統認知誤區。咱們給函數塊命名主要依據系統中的功能角色。

開發者有意識的根據設計模式組織他們的代碼,而不是根據設計模式自動去組織。不管哪種,都沒有什麼問題。可是在本文中,我說明在WPF程序中明確使用MVVM模式的好處。

某些類的名稱,包括MVVM模式中著名的術語,若是類是View的抽象類就以ViewModel結束。這種方式有助於避免以前提到的認知誤區。相反,你也可讓那種受控的誤區存在,這正是大部分軟件開發項目的自熱狀態。

3模型-視圖-視圖模型的演變

自從人們開始構建UI時,就有不少流行的設計模式讓UI構建更容易。好比,MVP模式在各類UI編程平臺中都很是流行。MVP是MVC模式的一種變體,MVC模式已經流行了幾十年了。以防你以前從沒用過MVP模式,這裏作一個簡單的解釋。你在屏幕上看到的是View,它顯示的數據是Model,Presenter就是把二者聯繫起來。View依賴Presenter並經過Presenter展現Model數據,響應用戶輸入,提供數據驗證(或許委託給Model去完成)以及其餘的一些任務。若是你想了解更過關於MVP模式,我建議你去讀Jean-Paul Boodhoo的August 2006 Design Patterns column

2004年晚些時候,Martin Fowler發表了一篇叫Presentation Model(PM)的模式。PM模式和MVP相似,MVP是把一個View從行爲和狀態分離出來。PM中使人關注的部分是建立view的抽象,叫作Presentation Model。以後,View就僅僅是Presentation Model的展現了。在Fowler的論文中,他展現了Presentation Model常常更新View,以便兩個彼此同步。同步邏輯組做爲代碼存在於Presentation Model類中。

2005年,John Gossman,目前是微軟WPF和Silverlight架構師,在他的博客上披露了Model-View-ViewModel (MVVM)模式。MVVM和Fowler的Presentation Model是一致的,兩個模式的特徵都是View的抽象,都包含了View的行爲和狀態。Fowler引入Presentation Model是做爲建立獨立平臺的View的抽象,而Gossman引入MVVM是做爲標準化的方法,利用WPF的核心特色去簡化UI的建立。從這種意義上來說,我把MVVM做爲通常PM模式的一個特例。

在Glenn Block一遍優秀的文章"Prism: Patterns for Building Composite Applications with WPF",於2008年9月微軟大會發布,他解釋了WPF微軟組合程序開發嚮導。術語ViewModel沒有用到,然而PM卻用來描述View的抽象。這篇文章自始至終,都沒沒有出現我要將MVVM模式,以及View的抽象ViewModel。我發現這個術語在WPF和Silverlight社區中比較流行。

不像MVP中的Presenter,ViewModel不須要引用View。View 綁定ViewModel的屬性,ViewMode向Viewl暴露Model對象的數據以及其餘的狀態。View和ViewModel之間的綁定很容易構造,由於ViewModel對象能夠設置爲View的DataContext。若是ViewModel中的屬性值發生改變,新值將經過綁定自動傳送給View。當用戶點擊View中的按鈕時,ViewMode對於的Command將執行請求的動做。ViewModel,毫不是View,去執行實體對象的修改。

View類並不知道Model類是否存在,同時ViewModel和Model也不知道View。實際上,,Model徹底不知道ViewModel和View存在,這是一個很是鬆耦合的設計,在不少方面都有好處,這不就你就會看到。

4爲何WPF開發者喜歡MVVM

一旦開發者適應了WPF和MVVM,就很難區別二者。由於MVVM很是適合WPF平臺,而且WPF被設計使用MVVM模式更容易構建應用程序,MVVM就成了WPF開發者的通用語。事實上,微軟內部正在用MVVM開發WPF應用程序,像Microsoft Expression Blend,然而,當時WPF平臺的核心功能依然在開發之中。WPF的不少方面,像控制模型以及數據模版,都利用了MVVM推薦的顯示狀態和行爲分離技術。

MVVM之因此成爲一個偉大設計模式,是由於WPF的一個最重要的特徵數據綁定構造。經過把Viewde 屬性綁定到ViewModel,你就能夠獲得二者鬆耦合的設計,而且徹底去除ViewModel更新View的那部分代碼。數據綁定系統支持輸入驗證,而且輸入驗證提供了傳遞錯誤給View的標準方法。

另兩個WPF的特色,數據模版資源系統讓MVVM模式更加可用。數據模版把View應用在ViewModel對象上,以便其可以在UI上顯示。你能夠在Xaml中聲明模版,讓資源系統在系統運行過程當中自動定位並應用這些模版。你能夠從我2008年7月寫的一篇文章, "Data and WPF: Customize Data Display with Data Binding and WPF.",獲取更多關於綁定和數據模版的信息。

要不是WPF對Command的支持,MVVM模式就不會那麼強大。本文中,我會爲你展現ViewModel怎樣把Commands暴露給View,而且讓View消費它的功能。若是你對Command不是很熟悉,我推薦你讀一下2008年9月Brian Noyes發佈的文章, "Advanced WPF: Understanding Routed Events and Commands in WPF"。

除了WPF(Silverlight2)自己讓MVVM以一種天然的方式去構建程序以外,形成MVVM模式流行還有一個緣由,那就是ViewModel類很容易進行單元測試。從某種意義來說,View和單元測試只是ViewModel兩個不一樣類型的消費者。擁有一套應用程序的單元測試,能夠爲提供更自由、快速的迴歸測試,而回歸測試有助於下降以後應用的維護成本。

除了促進建立自動化迴歸測試外,ViewModel類的可測試性也有助於設計更容易分離的UI。當你設計應用時,你能夠經過想象某些東西是否要建立單元測試消費ViewModel,來肯定它們是放到View裏面仍是ViewModel裏面。若是你能夠爲ViewModel寫單元測試而不用建立任何UI控件,你也能夠把ViewModel剝離出來,由於它不依賴任何具體可視化的組件。

最後,對於要和設計者合做的開發者來講,使用MVVM模式使得建立平滑的開發/設計工做流更加容易。既然View能夠是ViewModel的任意一個消費者,就很容易去掉一個View經過新增一個View去渲染ViewModel。這個簡單的步驟容許設計師構建快速原型以及評估UI設計。

這樣開發團隊能夠關注建立健壯的ViewModel類,而設計團隊能夠關注設計界面友好的View。要融合兩個團隊輸出只須要在View的xaml上進行正確的綁定便可。

5演示程序

到此爲止,咱們回顧了MVVM的歷史以及具體操做理論。我也說明了它在WPF開發者中間如此流行的緣由。如今是時候繼續咱們的步伐,看一下MVVM模式在實際中的應用。這篇文章中的演示程序以各類方式使用MVVM設計模式,它提供了豐富的例子,幫助在上下文中理解MVVM的概念。我用VS2008 SP1建立的這個演示程序, 框架是Microsoft .NET Framework 3.5 SP1。單元測試是用的Visual Studio unit testing。

應用能夠包含任意數量的「Workspace」,每個均可以由用戶點擊左側導航區的命令連接打開。全部的Workspace寄宿在主區域TabControl中,用戶能夠經過點擊Workspace的 tab item上關閉按鈕關閉Workspace。應用程序有兩個可用的Workspace:"All Customers" 和 "New Customer"。運行程序,打開一些Workspace,UI看起來如圖1所示。

 

圖1 Workspaces

一次只有一個All Customers Workspace的實例能夠打開,可是能夠打開多個New Customer Workspace。當用戶決定建立一個新客戶時,她必須填完圖2所示的數據輸入表單。

 

圖2 新客戶數據輸入表單

填完數據輸入表單的全部有效值點擊「Save」按鈕,新客戶的名稱將會出如今tab item 上面,同時新客戶也會增長到客戶列表中。應用程序不支持刪除或者編輯客戶,可是這和其它功能相似,很容易在已有的程序架構上去實現。如今你已經對演示程序有了更深層次的理解了,接下來咱們研究它是如何設計以及實現的。

5.1中繼命令邏輯(Relaying Command Logic)

除了類構造器裏調用初始化組件標準的樣板代碼,應用中的每一View的code-behind文件都是空的。實際上你能夠移除View的code-behind文件,程序讓人可以爭正確的編譯和運行。儘管View中沒有事件處理方法,可是當用戶點擊按鈕時,程序依然可以響應並知足用戶的請求。之因此這樣,是由於UI上Hyperlink、 Button以及MenuItem控件的Command屬性被綁定了。綁定機制確保當用戶在控件上點擊時,由ViewModel暴露的ICommand對象可以執行。你能夠把command對象看做一個適配器,這個適配器讓command對象很容易消費在View中聲明的ViewModel功能。

當ViewModel暴露ICommad類型的實例屬性,被暴露的Command對象使用ViewModel中的對象去完成它的工做。其中一個可能的實現模式是在ViewModel內建立一個私有嵌套類,以便command可以訪問包含在ViewModel中的私有成員,而不至於污染命名空間。嵌套類實現了ICommand接口,包含在ViewModel中對象的引用注入到其構造器中。可是爲ViewModel暴露的每一個Command建立實現ICommad的嵌套類,會增長ViewModel類的大小。更多的代碼意味着存在BUGS潛力更大。

在演示程序中,RelayCommand類解決了這個問題。RelayCommand容許經過把委託傳給其構造器,以實現對命令邏輯的注入。這種方式容許在ViewMode類中能夠簡單明瞭的實現Command。

RelayCommand是DelegateCommand的一個簡單的變體,DelegateCommand能夠在Microsoft Composite Application Library找到。RelayCommand類代碼如圖3所示。

 

圖3 RelayCommand類

public class RelayCommand : ICommand

{

    #region Fields

    readonly Action<object> _execute;

    readonly Predicate<object> _canExecute;

    #endregion // Fields

    #region Constructors

    public RelayCommand(Action<object> execute)

    : this(execute, null)

    {

    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)

    {

        if (execute == null)

            throw new ArgumentNullException("execute");

        _execute = execute;

        _canExecute = canExecute;

    }

    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]

    public bool CanExecute(object parameter)

    {

        return _canExecute == null ? true : _canExecute(parameter);

    }

    public event EventHandler CanExecuteChanged

    {

        add { CommandManager.RequerySuggested += value; }

        remove { CommandManager.RequerySuggested -= value; }

    }

    public void Execute(object parameter)

    {

        _execute(parameter);

    }

    #endregion // ICommand Members

}

做爲接口ICommad實現一部分,事件CanExecuteChanged有一些值得關注的特徵。它委託訂閱CommandManager. RequerySuggested事件。這樣以確保不管什麼時候調用內置命令時,WPF命令架構都能調用全部可以執行的RelayCommand對象。

RelayCommand _saveCommand;

public ICommand SaveCommand

{

    get

    {

        if (_saveCommand == null)

        {

            _saveCommand = new RelayCommand(param => this.Save(),

                param => this.CanSave );

        }

        return _saveCommand;

    }

}

5.2ViewModel類層級圖

大部分ViewModel類有共同的特徵,他們要實現INotifyPropertyChanged接口,須要顯示一個友好的名字,以以前說道Workspace爲例,它須要可以關閉(即從UI上移除)。要解決這個問題,天然就須要建立一個或二個ViewModel基類,以便新的ViewModel類可以從基類集成通用的功能。全部的ViewModel類造成如圖4的層級圖。

 

圖4 繼承層級圖

爲你的ViewModel建立一個基類並非必須。若是你喜歡在類中經過組合幾個小一點的類以得到那些功能,而不是用繼承的方式,這並無什麼問題。就像任何其餘的設計模式同樣,MVVM是一套指導方針,而不是規則。

5.3ViewModelBase 類

ViewModelBase 是層級中的根類,這就是它要實現通用INotifyPropertyChanged接口以及有一個DisplayName屬性的緣由。INotifyPropertyChanged接口包含一個叫PropertyChanged的事件。不管什麼時候ViewModel對象的屬性的發生改變時,它都會觸發PropertyChanged事件,把新值通知給WPF綁定系統。根據通知,綁定系統檢索屬性,UI組件上綁定的屬性將接受新值。

爲了讓WPF知道是那一個屬性發生了改變,PropertyChangedEventArgs類暴露了一個string類型的屬性PropertyName 。你必定要爲事件參數傳遞正確的屬性名,不然WPF將會爲新值檢索出一個錯誤的屬性。

ViewModelBase一個值得關注的地方就是它爲給定的屬性名提供了驗證,驗證屬性是否存在ViewModel對象上。重構時,這很是有用。由於經過VS 2008重構功能去改變屬性名,不會更新源代碼中字符串,而這些字符串正好包含屬性名(其實不該該包含)。在事件參數中傳遞不正確的屬性名,觸發PropertyChanged事件時,可能會致使微小的BUGs,而且這些BUGs很難追蹤,所以這個細微的特徵將會節省大量的時間。ViewModelBase中增長了這個有用的特徵,其代碼以下:

 

圖5 屬性驗證

// In ViewModelBase.cs

public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)

{

    this.VerifyPropertyName(propertyName);

    PropertyChangedEventHandler handler = this.PropertyChanged;

    if (handler != null)

    {

        var e = new PropertyChangedEventArgs(propertyName);

        handler(this, e);

    }

}

 

[Conditional("DEBUG")]

[DebuggerStepThrough]

public void VerifyPropertyName(string propertyName)

{

    // Verify that the property name matches a real, 

    // public, instance property on this object.

    if (TypeDescriptor.GetProperties(this)[propertyName] == null)

    {

        string msg = "Invalid property name: " + propertyName;

        if (this.ThrowOnInvalidPropertyName)

            throw new Exception(msg);

        else

            Debug.Fail(msg);

    }

}

5.4CommandViewModel 類

CommandViewModel是最簡單的ViewModelBase子類,它暴露了一個類型爲ICommad的Command屬性。MainWindowViewModel經過Commands屬性暴露了CommandViewModel對象的一個集合。主窗口左手側的導航區域,顯示了MainWindowView­Model暴露每一個CommandViewModel對象連接,像「View all customers」和「Create new customer」。當用戶點擊連接,將會執行相應的Command,在主窗口的TabControl中打開一個workspace。CommandViewModel類的定義以下所示:

public class CommandViewModel : ViewModelBase

{

    public CommandViewModel(string displayName, ICommand command)

    {

        if (command == null)

            throw new ArgumentNullException("command");

        base.DisplayName = displayName;

        this.Command = command;

    }

    public ICommand Command { get; private set; }

}

在MainWindowResources.xaml文件中存在一個key爲CommandsTemplate的數據模版,主窗口(MainWindow)使用這個模版渲染以前提到的CommandViewModel對象集合。這個模版是簡單在ItemsControl裏把每一個CommandViewModel對象渲染成一個連接,每一個連接的Command屬性綁定到CommandViewModel對象的Command屬性。數據模版Xaml如圖6所示:

 

圖6 渲染Command列表

<!-- In MainWindowResources.xaml -->

<!--

This template explains how to render the list of commands on

the left side in the main window (the 'Control Panel' area).

-->

<DataTemplate x:Key="CommandsTemplate">

  <ItemsControl ItemsSource="{Binding Path=Commands}">

    <ItemsControl.ItemTemplate>

      <DataTemplate>

        <TextBlock Margin="2,6">

          <Hyperlink Command="{Binding Path=Command}">

            <TextBlock Text="{Binding Path=DisplayName}" />

          </Hyperlink>

        </TextBlock>

      </DataTemplate>

    </ItemsControl.ItemTemplate>

  </ItemsControl>

</DataTemplate>

5.5MainWindowViewModel 類

如前面看到的類圖同樣,WorkspaceViewModel類繼承於ViewModelBase並增長了「關閉」的能力。這個「關閉」,個人意思是在運行的時候能把workspace從UI上移除。有三個類繼承於WorkspaceViewModel,他們分別爲MainWindowViewModel,AllCustomersViewModel和CustomerViewModel。MainWindowViewModel的關閉請求是由App類處理的,其中App類建立了MainWindow以及它對應的ViewModel對象。建立代碼如圖7所示.

 

圖7 建立ViewModel

// In App.xaml.cs

protected override void OnStartup(StartupEventArgs e)

{

    base.OnStartup(e);

    MainWindow window = new MainWindow();

    // Create the ViewModel to which

    // the main window binds.

    string path = "Data/customers.xml";

    var viewModel = new MainWindowViewModel(path);

    // When the ViewModel asks to be closed,

    // close the window.

    viewModel.RequestClose += delegate

    {

        window.Close();

    };

    // Allow all controls in the window to

    // bind to the ViewModel by setting the

    // DataContext, which propagates down

    // the element tree.

    window.DataContext = viewModel;

    window.Show();

}

MainWindow包含一個菜單項,該菜單項的Command屬性綁定到MainWindowViewModel上的CloseCommand屬性上。當用戶點擊該菜單,App類響應請求,調用窗體的關閉方法。菜單Xaml以下所示:

<!-- In MainWindow.xaml -->

<Menu>

  <MenuItem Header="_File">

    <MenuItem Header="_Exit" Command="{Binding Path=CloseCommand}" />

  </MenuItem>

  <MenuItem Header="_Edit" />

  <MenuItem Header="_Options" />

  <MenuItem Header="_Help" />

</Menu>

MainWindowViewModel包含了WorkspaceViewModel對象一個observable類型的集合,該集合的名稱爲Workspaces。主窗體包含了一個TabControl,其ItemsSource綁定到上述的集合。每個tab item都有一個關閉按鈕,其Command屬性綁定到它對應WorkspaceViewModel實例的CloseCommand上。模版展現瞭如何渲染一個帶關閉按鈕的tab item。配置tab item模版的簡化版會展現在下面代碼中,這段代碼能夠在MainWindowResources.xaml文件中找到。

<DataTemplate x:Key="ClosableTabItemTemplate">

  <DockPanel Width="120">

    <Button

      Command="{Binding Path=CloseCommand}"

      Content="X"

      DockPanel.Dock="Right"

      Width="16" Height="16"

      />

    <ContentPresenter Content="{Binding Path=DisplayName}" />

  </DockPanel>

</DataTemplate>

當用戶點擊tab item上的關閉按鈕時,會執行Workspace­ViewModel的CloseCommand,觸發它的Request­Close事件。MainWindowViewModel會監控workspace的Request­Close事件,根據請求從Workspaces集合中移除相應的workspace。由於Main­Window的TabControl的ItemsSource綁定到Workspace­ViewModel的observable集合,從集合中移除對象,會引發從TabControl中移除相應的workspace。Main­WindowViewModel相應的邏輯如圖8所示

 

圖8 從UI上移除workspace

// In MainWindowViewModel.cs

ObservableCollection<WorkspaceViewModel> _workspaces;

public ObservableCollection<WorkspaceViewModel> Workspaces

{

    get

    {

        if (_workspaces == null)

        {

            _workspaces = new ObservableCollection<WorkspaceViewModel>();

            _workspaces.CollectionChanged += this.OnWorkspacesChanged;

        }

        return _workspaces;

    }

}

 

void OnWorkspacesChanged(object sender, NotifyCollectionChangedEventArgs e)

{

    if (e.NewItems != null && e.NewItems.Count != 0)

        foreach (WorkspaceViewModel workspace in e.NewItems)

            workspace.RequestClose += this.OnWorkspaceRequestClose;

    if (e.OldItems != null && e.OldItems.Count != 0)

        foreach (WorkspaceViewModel workspace in e.OldItems)

            workspace.RequestClose -= this.OnWorkspaceRequestClose;

}

 

void OnWorkspaceRequestClose(object sender, EventArgs e)

{

    this.Workspaces.Remove(sender as WorkspaceViewModel);

}

在UnitTests項目中,MainWindowViewModelTests.cs文件包含了一個測試方法,該方法驗證上述功能是否正確執行。很容易爲ViewModel類建立單元測試是MVVM模式的一個大賣點,由於它只需測試應用程序的功能,而不用寫和UI交互的代碼。上述測試方法圖9所示

 

圖9 測試方法

// In MainWindowViewModelTests.cs

[TestMethod]

public void TestCloseAllCustomersWorkspace()

{

    // Create the MainWindowViewModel, but not the MainWindow.

    MainWindowViewModel target =

        new MainWindowViewModel(Constants.CUSTOMER_DATA_FILE);

    Assert.AreEqual(0, target.Workspaces.Count, "Workspaces isn't empty.");

    // Find the command that opens the "All Customers" workspace.

    CommandViewModel commandVM =

        target.Commands.First(cvm => cvm.DisplayName == "View all customers");

    // Open the "All Customers" workspace.

    commandVM.Command.Execute(null);

    Assert.AreEqual(1, target.Workspaces.Count, "Did not create viewmodel.");

    // Ensure the correct type of workspace was created.

    var allCustomersVM = target.Workspaces[0] as AllCustomersViewModel;

    Assert.IsNotNull(allCustomersVM, "Wrong viewmodel type created.");

    // Tell the "All Customers" workspace to close.

    allCustomersVM.CloseCommand.Execute(null);

    Assert.AreEqual(0, target.Workspaces.Count, "Did not close viewmodel.");

}

5.6把View應用到ViewModel上

MainWindowViewModel間接從主窗體的Tab­Control控件中增長移除Workspace­ViewModel對象。經過數據綁定,TabItem的Content屬性顯示繼承於ViewModelBase的對象。ViewModelBase並非一個UI元件,所以他並不支持渲染它本身。默認在TextBlock中,WPF的一個非可視化對象經過調用ToString方法以顯示該對象。很明顯這不是你想要的,除非你的用戶迫切的想知道ViewModel的類型名。

咱們經過強類型數據模版很容易告訴WPF如何渲染ViewModel對象。強類型數據模版key屬性名沒有賦值,可是其DataType屬性要賦以類型類的實例。若是WPF要去渲染ViewModel對象,它會檢查在資源系統範圍內是否有一個強類型數據模版的DataType和ViewModel對象(或者其基類)的類型同樣。若是找到一個這樣的模版的話,他會用該模版去渲染被TabItem Content屬性綁定的ViewModel對象。

MainWindowResources.xaml文件中有一個Resource­Dictionary(資源字典),該字典被增長到主窗體的資源層級中,這意味着文件包含的資源在正窗體範圍內有效。當一個TabItem的Content屬性設置ViewModel對象時,該字典中的強類型數據模版會提供一個View(即用戶自定義控件)去渲染TabItem Content。具體如圖10所示

 

圖10 提供View

<!--

This resource dictionary is used by the MainWindow.

-->

<ResourceDictionary

  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

  xmlns:vm="clr-namespace:DemoApp.ViewModel"

  xmlns:vw="clr-namespace:DemoApp.View"

  >

  <!--

  This template applies an AllCustomersView to an instance

  of the AllCustomersViewModel class shown in the main window.

  -->

  <DataTemplate DataType="{x:Type vm:AllCustomersViewModel}">

    <vw:AllCustomersView />

  </DataTemplate>

  <!--

  This template applies a CustomerView to an instance 

  of the CustomerViewModel class shown in the main window.

  -->

  <DataTemplate DataType="{x:Type vm:CustomerViewModel}">

    <vw:CustomerView />

  </DataTemplate>

 <!-- Other resources omitted for clarity... -->

</ResourceDictionary>

你不須要寫任何代碼去決定哪個View去展現ViewModel對象。WPF資源系統把你從繁重的工做解脫出來,讓你去關注更重要的事情。在複雜的場景中,可能須要經過編程去選擇View,可是在大部分狀況下,經過編程選擇View是沒必要要的。

5.7The Data Model and Repository數據模型和存儲庫

你已經知道應用程序如何去加載,顯示以及關閉一個ViewModel對象。如今一切已經就位,你能夠在整個應用程序範圍內,回顧一下具體實現的細節。在深刻理解應用程序的兩個workspace,「All Customers」 和 「New Customer」以前,咱們先審視一下數據模型和數據存取類。這些類的設計和MVVM模式並無什麼關係,由於你能夠建立一個ViewModel類,以適應任何對WPF友好的數據對象。

演示程序中惟一的模型類Customer類,該類有些屬性表徵一個公司的客戶,像他們的姓名,email等。它經過實現IDataErrorInfo接口提供屬性驗證信息,該接口在WPF大行其道以前已存在多年。Customer類裏面並無什麼,並非建議其用於MVVM架構,或者甚至應用和WPF應用程序。這個類很容易從遺留的業務庫中獲取。

數據必須存到某個地方,在這個應用程序中,CustomerRepository類的實例加載並存儲全部的Customer對象。該CustomerRepository從xml文件加載全部的客戶數據,這與外部的數據源無關。數據可能來自數據庫、Web服務、命名管道、磁盤上的文件甚至信鴿,這並無什麼關係。只要你有一個有數據.net對象,無論它來自何方,MVVM模式都能在屏幕上獲取其包含數據。

CustomerRepository類暴露了一些方法,這些方法容許你獲取全部的Customer對象,增長一個Customer對象到存儲室並檢查其子啊存儲室是否存在。既然應用程序不容許刪除客戶,存儲室也不容許你去刪除客戶。當一個新Customer經過AddCustomer方法增長到CustomerRepository時,會觸發CustomerAdded事件。

很明顯,和真實業務程序所需的相比,該程序的數據模型是輕量級的,可是這並無關係。重要的是,要理解ViewModel類如何利用Customer和CustomerRepository類。知道Customer­ViewModel是對Customer對象的封裝,其經過一系列的屬性暴露了Customer的狀態,以及被Customer­View使用的狀態。CustomerViewModel並非複製Customer對象的狀態,而是經過委託暴露這狀態,具體以下:

public string FirstName

{

    get { return _customer.FirstName; }

    set

    {

        if (value == _customer.FirstName)

            return;

        _customer.FirstName = value;

        base.OnPropertyChanged("FirstName");

    }

}

當用戶在CustomerView控件中建立新客戶點擊保存按鈕時,與該視圖關聯的CustomerViewModel會增長一個Customer對象到Customer­Repository 。這會觸發存儲庫的CustomerAdded事件,該事件讓AllCustomers­ViewModel知道他應該增長一個Customer­ViewModel對象到AllCustomers集合。從某種意義說,Customer­Repository在各自ViewModel和他們要處理的Customer對象間扮演數據同步的角色,或許有人會把這當成中介者模式。我會在接下來的內容中介紹其實現機理,可是爲了更進一步瞭解這些類如何鏈接在一塊兒,咱們如今先看一下圖11所示的類圖

圖11 Customer關係圖

5.8New Customer Data Entry Form新增客戶數據輸入表單

當用戶點擊「Create new customer」連接,MainWindowViewModel會增長一個CustomerViewModel到workspaces集合,相應的CustomerView回去顯示。用戶在輸入框輸入有效值以後,Save按鈕變爲可用狀態,以便用戶能存儲增長客戶信息。這並無超出常規的地方,只是一個帶有驗證信息和Save按鈕的常規輸入表單而已。

Customer類內置輸入驗證支持,這是經過實現IDataErrorInfo接口得到。輸入驗證確保客戶有一個名字,合法的email地址,若是客戶是我的客戶,還須要姓氏。若是Customer對象的IsCompany屬性爲真,則其LastName屬性不能有值。該驗證邏輯從Customer對象的角度看是有意義的,可是它並不能知足UI的須要,UI要求用戶選擇客戶類別是我的仍是公司。Customer類別選擇器初始值是: (Not Specified),若是Customer對象的IsCompany屬性只容許是true和false,客戶類別是unspecified時,UI如何告訴用戶?

假定你對整個軟件系統擁有控制權限,你能夠把IsCompany屬性類型改變爲Nullable<bool>,該類型容許「未選擇」值(即空值-譯者注)。然而在現實世界中,不是這麼簡單。假設你不能改變Customer類,由於它來自公司其餘團隊所開發系統。要是由於數據庫的緣由,沒有簡單方法存儲未選擇的值,怎麼辦?要是其它程序已經使用Customer類,而且其依賴正常Boolean類型的IsCompany屬性,怎麼辦?諸如此類,可使用ViewModel去解決。

圖12所示的測試方法展現了該功能如何在CustomerViewModel中工做,CustomerViewModel暴露了一個CustomerTypeOptions屬性,以便UI上的客戶類型選擇器有三個字符串顯示。同時它也暴露了一個CustomerType屬性,該屬性存放選擇器選中的字符串。當CustomerType被賦值時,它會潛在的Customer對象IsCompany屬性,把字符類型轉化爲Boolean類型。圖13展現了這兩個屬性。

 

圖12 測試方法

// In CustomerViewModelTests.cs

[TestMethod]

public void TestCustomerType()

{

    Customer cust = Customer.CreateNewCustomer();

    CustomerRepository repos = new CustomerRepository(

        Constants.CUSTOMER_DATA_FILE);

    CustomerViewModel target = new CustomerViewModel(cust, repos);

    target.CustomerType = "Company"

    Assert.IsTrue(cust.IsCompany, "Should be a company");

    target.CustomerType = "Person";

    Assert.IsFalse(cust.IsCompany, "Should be a person");

    target.CustomerType = "(Not Specified)";

    string error = (target as IDataErrorInfo)["CustomerType"];

    Assert.IsFalse(String.IsNullOrEmpty(error), "Error message should

        be returned");

}

 

圖13 CustomerTypeOptions和CustomerType

// In CustomerViewModel.cs

public string[] CustomerTypeOptions

{

    get

    {

        if (_customerTypeOptions == null)

        {

            _customerTypeOptions = new string[]

            {

                "(Not Specified)",

                "Person",

                "Company"

            };

        }

        return _customerTypeOptions;

    }

}

public string CustomerType

{

    get { return _customerType; }

    set

    {

        if (value == _customerType ||

            String.IsNullOrEmpty(value))

            return;

        _customerType = value;

        if (_customerType == "Company")

        {

            _customer.IsCompany = true;

        }

        else if (_customerType == "Person")

        {

            _customer.IsCompany = false;

        }

        base.OnPropertyChanged("CustomerType");

        base.OnPropertyChanged("LastName");

    }

}

CustomerView用戶控件中有一個ComboBox綁定這兩個屬性,以下所示:

<ComboBox

  ItemsSource="{Binding CustomerTypeOptions}"

  SelectedItem="{Binding CustomerType, ValidatesOnDataErrors=True}"

/>

當ComboBox中的選擇項發生改變時,其數據源的將會掃描IDataErroInfo接口查看新值是否有效。之因此這樣,是由於綁定的SelectedItem屬性有一個ValidateOnDataErrors設置爲true。既然數據源是一個CustomerViewModel對象,綁定系統會會向CustomerViewModel對象要求一個對CustomerType屬性的驗證信息。大多狀況下,CustomerViewModel會把全部的驗證請求委託給其包含的Customer對象。然而,由於Customer的IsCompany屬性沒有未選中狀態的概念,因此CustomerViewModel必須對ComboBox中新選擇項進行處理。具體代碼如圖14所示。

 

圖14 驗證CustomerViewModel對象

// In CustomerViewModel.cs

string IDataErrorInfo.this[string propertyName]

{

    get

    {

        string error = null;

        if (propertyName == "CustomerType")

        {

            // The IsCompany property of the Customer class

            // is Boolean, so it has no concept of being in

            // an "unselected" state. The CustomerViewModel

            // class handles this mapping and validation.

            error = this.ValidateCustomerType();

        }

        else

        {

            error = (_customer as IDataErrorInfo)[propertyName];

        }

        // Dirty the commands registered with CommandManager,

        // such as our Save command, so that they are queried

        // to see if they can execute now.

        CommandManager.InvalidateRequerySuggested();

        return error;

    }

}

 

string ValidateCustomerType()

{

    if (this.CustomerType == "Company" ||

       this.CustomerType == "Person")

        return null;

    return "Customer type must be selected";

}

該部分代碼重點在於CustomerViewModel實現了IDataErrorsInfo接口,能夠處理對CustomerViewModel具體屬性的驗證請求,同時把其它請求委託給Customer對象處理。這樣容許咱們使用Model類的驗證邏輯,其它屬性驗證在ViewModel類中才有意義。

經過SaveCommand屬性去保存CustomerViewModel對象,該命令使用了以前陳述的RelayCommand,容許CustomerViewModel決定其是否能保存本身以及被告知保存其狀態時作什麼。在該程序中,保存一個新客戶只是把其增長到CustomerRepository。決定一個新客戶是否可以保存,須要兩方面的許可,一是Customer對象是否有效,二是CustomerViewModel必須是有效的。這兩方面是必要條件,因爲前面陳述的ViewModel其特定屬性以及Customer對象驗證信息。CustomerViewModel的保存邏輯如圖15所示

 

圖15 CustomerViewModel的保存邏輯

// In CustomerViewModel.cs

public ICommand SaveCommand

{

    get

    {

        if (_saveCommand == null)

        {

            _saveCommand = new RelayCommand(

                param => this.Save(),

                param => this.CanSave

                );

        }

        return _saveCommand;

    }

}

 

public void Save()

{

    if (!_customer.IsValid)

        throw new InvalidOperationException("...");

    if (this.IsNewCustomer)

        _customerRepository.AddCustomer(_customer);

    base.OnPropertyChanged("DisplayName");

}

 

bool IsNewCustomer

{

    get

    {

        return !_customerRepository.ContainsCustomer(_customer);

    }

}

 

bool CanSave

{

    get

    {

        return String.IsNullOrEmpty(this.ValidateCustomerType()) &&_customer.IsValid;

    }

}

這裏ViewModel的使用使得建立顯示Customer對象的View更加容易,而且容許像Boolean類型未選中這樣事情存在。同時它很容易告訴客戶保存其狀態。若是View直接綁定到Customer對象,View將會須要不少代碼才能恰當的工做。在一個設計良好的MVVM架構中,大部分View的背後代碼應該爲空,或者最多隻包含操縱View內的控件以及資源的代碼。有時在View後面寫一些代碼也是必須的,由於要和ViewModel對象進行交互,像傳遞事件或者調用方法不然從ViewModel作些事情很難。

5.9All Customers View全部客戶視圖

演示程序也包含了一個在ListView中顯示全部客戶列表的workspace。這些客戶經過根據其是我的客戶仍是公司客戶進行分組。用戶一次能夠選擇一個或者多個客戶,在右下方查看其總銷售額。

該UI是AllCustomersView控件,用以渲染AllCustomersViewModel對象。每一個ListView­Item表明一個CustomerViewModel對象,該對象存在於AllCustomerViewModel對象暴露的AllCustomers集合中。在前一部分,你看到CustomerViewModel如何渲染成數據輸入表單,而如今如出一轍的CustomerViewModel對象卻被渲染成ListView中的一個Item。CustomerViewModel並不知道那一個可視化的組件去顯示它,這使得其重用成爲可能。

AllCustomersView建立了在ListView中看到的分組,這是經過把ListView的ItemsSource綁定到配置如圖16所示的Collection­ViewSource中實現的。

圖16 CollectionViewSource

<!-- In AllCustomersView.xaml -->

<CollectionViewSource

  x:Key="CustomerGroups"

  Source="{Binding Path=AllCustomers}"

  >

  <CollectionViewSource.GroupDescriptions>

    <PropertyGroupDescription PropertyName="IsCompany" />

  </CollectionViewSource.GroupDescriptions>

  <CollectionViewSource.SortDescriptions>

    <!--

    Sort descending by IsCompany so that the ' True' values appear first,

    which means that companies will always be listed before people.

    -->

    <scm:SortDescription PropertyName="IsCompany" Direction="Descending" />

    <scm:SortDescription PropertyName="DisplayName" Direction="Ascending" />

  </CollectionViewSource.SortDescriptions>

</CollectionViewSource>

ListViewItem和CustomerViewModel之間的關聯是經過ListView的ItemContainerStyle屬性創建的。指定給該屬性的Style應用於每一個ListViewItem,這使得ListViewItem的屬性能夠綁定到CustomerViewModel對象的屬性上。這個Style一個重要的綁定就是在ListViewItem的IsSelected屬性和CustomerViewModel的IsSelected屬性之間創建的聯繫,以下所示:

<Style x:Key="CustomerItemStyle" TargetType="{x:Type ListViewItem}">

  <!--   Stretch the content of each cell so that we can

  right-align text in the Total Sales column.  -->

  <Setter Property="HorizontalContentAlignment" Value="Stretch" />

  <!-- Bind the IsSelected property of a ListViewItem to the IsSelected property of a CustomerViewModel object.-->

  <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />

</Style>

當CustomerViewModel對象被選中仍是未選中,會引發全部選中客戶銷售總額發生改變。AllCustomersViewModel負責維護總銷售額,以便ListView下部的ContentPresenter顯示正確的數字。圖17顯示AllCustomersViewModel如何監控被選中或未選中的每一個客戶,並通知View更新要顯示的值。

圖17 監控選中或未選中的客戶

// In AllCustomersViewModel.cs

public double TotalSelectedSales

{

  get

  {

    return this.AllCustomers.Sum(custVM=>custVM.IsSelected ? custVM.TotalSales : 0.0);

  }

}

 

void OnCustomerViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)

{

    string IsSelected = "IsSelected";

    // Make sure that the property name we're

    // referencing is valid.  This is a debugging

    // technique, and does not execute in a Release build.

    (sender as CustomerViewModel).VerifyPropertyName(IsSelected);

    // When a customer is selected or unselected, we must let the

    // world know that the TotalSelectedSales property has changed,

    // so that it will be queried again for a new value.

    if (e.PropertyName == IsSelected)

        this.OnPropertyChanged("TotalSelectedSales");

}

UI綁定了TotalSelectedSales屬性,並把該值置爲貨幣格式。ViewModel對象,而不是View,經過返回TotalSelectedSales屬性Double類型值的字符串形式,設置其貨幣格式。.NET Framework 3.5 SP1 爲ContentPresenter增長了ContentStringFormat屬性,若是你使用更老版本的WPF,你須要在代碼中設置貨幣格式。

<!-- In AllCustomersView.xaml -->

<StackPanel Orientation="Horizontal">

  <TextBlock Text="Total selected sales: " />

  <ContentPresenter

    Content="{Binding Path=TotalSelectedSales}"

    ContentStringFormat="c"/>

</StackPanel>

6Wrapping Up總結

WPF爲應用程序開發者提供了不少,學習利用WPF賦予的力量,但須要轉變思惟模式。MVVM模式是設計和開發WPF程序的一種簡單而又有效的一套指導方針。它容許你建立數據、行爲和展現強分離的程序,這更容易控制軟件開發中的混亂因素。

相關文章
相關標籤/搜索