本文章是由機器翻譯。

模式

WPF 與 「 模型-檢視-ViewModel 設計 」 模式的應用程式

Josh Smith

本文將告訴您:

  • 模式和 WPF
  • MVP 模式
  • 為什麼會更好的 WPF 的 MVVM
  • 建置應用程式與 MVVM
本文將使用下列技術:
WPF,資料繫結

可從 MSDN 程式庫 的程式碼下載
瀏覽線上的程式碼

內容

排序與比較。chaos
模型-檢視-ViewModel 的發展
為什麼 WPF 項 o 人員愛 MVVM
示範應用程式
轉送命令的邏輯
ViewModel 類別階層架構
ViewModelBase 類別
CommandViewModel 類別
MainWindowViewModel 類別
將資料檢視套用一個 ViewModel
資料模型和存放庫
新的客戶資料項目表單
所有的客戶檢視
向上換行

「 開發使用者介面 的專業人員的軟體應用程式並不容易。它可以是資料、 互動設計、 視覺化設計、 連線多執行緒處理、 安全性、 國際化、 驗證、 單元測試,和 Voodoo 的一個觸控式在 murky 混用。請考慮一個的使用者介面公開 (Expose) 基礎的系統,和必須滿足其使用者的預期樣式的需求,它可以是許多應用程式的最動態區域。

有受歡迎的設計模式,可以協助 tame 這個不易的猛獸,但正確分隔,並解決的考量,許多可能會很困難。在更複雜的模式是,越可能的快速鍵將會使用稍後的破壞所有先前的工作,正確的方法中執行動作。

它不一定在錯誤的設計模式。有時候我們會使用需要撰寫大量的程式碼,因為在使用 UI 平台不強化本身也會設為簡單的模式的複雜的設計模式。內容的需要,是平台,可讓您輕鬆建置使用簡單、 time-tested、 開發人員所核准的設計模式的 UI。幸運的是,Windows Presentation Foundation (WPF) 提供完全的。

世界持續增加的速度,採用 WPF,軟體,WPF 社群有已經開發自己的生態系統模式和作法。在這的篇文章我將會檢閱一些設計和實作使用 WPF 的用戶端應用程式的最佳作法。我將會利用 WPF 與模型-檢視-ViewModel (MVVM) 的設計模式搭配某些核心功能逐步的範例程式,示範只簡單這可能是建置一個 WPF 應用程式 」 正確方法 」。

這篇文章結尾它將會清除如何資料的範本、 命令、 資料繫結、 資源的系統和 MVVM 模式所有符合一起來建立簡單、 測試、 穩固架構的任何 WPF 應用程式可以繁榮。本文隨附的示範程式可以做為真正的 WPF 應用程式使用 [MVVM] 做為其核心架構在範本中。示範方案中的單元測試會顯示測試的應用程式的使用者介面功能,該功能會存在於一組 ViewModel 類別是多麼簡單。至詳細資料的投擲之前, 請讓我們來檢視為什麼您應該首先使用像 MVVM 模式。

順序與比較混亂

它是不必要和不良在一個簡單的"Hello,World!"程式中使用設計模式。任何需求的開發人員可以瞭解幾行程式碼,一目瞭然。不過,許多功能,在程式中,讓程式碼和移動部分的行數增加適當。最後,資料的系統與週期性的問題,它包含的複雜性鼓勵開發人員組織程式碼,如此一來會比較容易理解、 討論、 擴充,以及疑難排解。我們可以藉由套用已知的名稱,至 [原始程式碼中的特定實體 (Entity 降低認知的混亂,複雜的系統。我們決定要套用至一段程式碼中,藉由在系統中,考慮其功能的角色名稱。

開發人員通常會刻意結構而不是要讓模式 organically 出現在設計模式根據其程式碼。沒有任何錯誤其中一個的方法,但是在這的篇文章,我會檢查為一個 WPF 應用程式的架構,明確地使用 MVVM 的優點。特定類別的名稱會包含從 MVVM 模式例如結束 「 ViewModel 如果類別是抽象檢視的已知的文字。這個方法會有助於避免認知先前所述的混亂。而,您可以幸好存在於這是最專業軟體開發專案的事務的自然狀態控制混亂的狀態!

模型-檢視-ViewModel 的發展

自不休人員會啟動來建立軟體的使用者介面中,有常用的設計模式,讓它更容易。例如,模型-檢視-主持人 (MVP) 模式已經讓不同的 UI 程式設計的平台上的普及。模型-檢視-控制器模式的已解決的十年的 MVP。您永遠不會有使用之前 MVP 模式,以下是簡單的說明。您在螢幕上所看到的是檢視的資料,它會顯示是模型,主持人一起連結兩個。檢視依賴於主持人填入模式資料、 回應使用者輸入、 提供 (也許是依據委派模型) 的輸入的驗證和其他這類的工作。如果您想進一步瞭解模型檢視主持人建議您閱讀 Jean-Paul Boodhoo2006 年 8 月的設計模式資料行.

在 2004,Martin Fowler 發行的命名模式的相關文章簡報模式(PM)。它將與它的行為和狀態中分隔檢視,PM 模式很類似 MVP。PM 模式的有趣部分是檢視的抽象概念建立,稱為簡報模型。檢視,然後,會變成只簡報模型的呈現。在 Fowler 的說明他顯示簡報模型的經常更新其檢視,讓兩個維持與彼此同步。以簡報的模型類別中的程式碼的形式存在的同步處理邏輯。

目前,WPF 和 Microsoft,Silverlight 的設計師的其中一個 John Gossman 在 2005,unveiled,模型-檢視-ViewModel (MVVM) 模式他的部落格。MVVM 等於 Fowler 的展示模型,兩個模式的功能檢視包含檢視的狀態和行為的抽象。fowler 引入的展示模型作為建立資料的檢視的一個 UI 平台獨立抽象的而 Gossman 會引入 MVVM 做為標準化方式使用核心功能,以簡化使用者介面的建立 WPF。在該種意義,我會考慮 MVVM 為一般 PM 模式,tailor-made WPF 和 Silverlight 平台的特製化。

Glenn 區塊的絕佳的文件 」 中建置複合應用程式,使用 WPF prism: 模式「 在 9 月 2008 的問題在他說明 Microsoft WPF 的複合應用程式指南。從未使用 ViewModel 一詞。而,詞彙的展示模型是可以用來描述在檢視的抽象概念。在這的篇文章不過,我將參照 MVVM 和檢視表的抽象概念,作為一個 ViewModel 模式。我發現這個術語是在 WPF 和 Silverlight 社群中有更多的 prevelant。

與主持人的 MVP,不同的是一個 ViewModel 不需要參考到檢視。檢視繫結至 ViewModel 的依次,公開的模型物件和其他的狀態設定為特定的檢視中包含資料的屬性。檢視] 和 [ViewModel 之間,繫結是簡單,因為一個 ViewModel 物件設定為檢視表的 DataContext。如果屬性值,在 [ViewModel 變更,這些新的值會自動傳播至透過資料繫結的檢視]。當使用者,按一下檢視中的按鈕時,ViewModel 上的命令會執行執行所要求的動作。在 ViewModel,永遠不會檢視會執行對模型資料所做的所有修改。

檢視類別不曉得模型類別有,模型與 ViewModel 時不知道的檢視。事實上,模型是完全 oblivious 到事實上,ViewModel] 和 [檢視存在。這是一個非常鬆散結合設計,這麼股利,許多的方式,您將會很快就看到的。

為什麼 WPF 項 o 人員愛 MVVM

一旦為開發人員熟悉 WPF 和 MVVM,則它可以很難區分兩個。MVVM 會是 WPF 的開發人員,成為 franca,因為很適合 WPF 平台,,WPF 為了輕鬆地建置應用程式使用 MVVM 模式 (中上其他)。事實,Microsoft 已使用 MVVM 在內部建構時,核心的 WPF 平台開發 WPF 的應用程式,例如 Microsoft Expression Blend。WPF,如外觀-小於控制項模型和資料範本的許多方面都使用顯示狀態和行為的 MVVM 升級強式的分隔。

在單一最重點,讓 MVVM 很棒的模式,使用 WPF 的是資料繫結基礎結構。繫結屬性,以在 ViewModel 檢視的您可以取得鬆散式兩者之間的連結,並完全移除需要撰寫程式碼中直接更新檢視的 ViewModel。資料繫結系統也會支援它提供一種標準化的方式傳輸到檢視驗證錯誤的輸入的驗證。

兩個其他功能 WPF 的讓此模式讓可用是資料的範本和資源) 系統。資料範本會套用檢視到使用者介面中顯示的 ViewModel 物件。您可以宣告中的 XAML 範本,並讓資源系統,自動找出並在執行階段,為您套用這些的範本。您可以瞭解更繫結] 及 [我的 7 月 2008 文章資料範本 」資料和 WPF: 使用資料繫結和 WPF 自訂資料顯示."

如果它沒有支援的命令在 WPF 中,MVVM 模式為較不強大。在這的篇文章我將示範如何在 ViewModel 可以公開命令來檢視,進而檢視,以使用其功能。若不熟悉命令我會建議您閱讀 Brian Noyes 完整文件 」已路進階 WPF: 瞭解由事件和 WPF 中的命令"從 9 月 2008 的問題。

除了在 WPF (和 Silverlight 2) 功能,讓 MVVM 結構的應用程式可以自然,模式也是常見,因為 ViewModel 類別很容易使用單元測試。當應用程式的互動邏輯會存在於一組 ViewModel 類別中時,您就可以輕鬆地撰寫程式碼,它會測試。在的觀念中檢視和單元測試只兩種不同類型的 ViewModel 消費者。有一組測試應用程式的 ViewModels 提供可用且快速回復測試的協助降低維護應用程式一段時間的成本。

除了升級的自動化的迴歸測試建立,ViewModel 類別的 testability 可以協助正確設計簡單面板的使用者介面。當您所設計的應用程式時, 您通常可以決定是否有應該是在檢視] 或 [由 imagining ViewModel,顯示您想要撰寫單元測試,以使用 [ViewModel。如果您可以在 ViewModel 的撰寫單元測試,而不建立任何的 UI 物件,可以也完全面板,ViewModel 因為它沒有相依性在特定的視覺元素上。

最後,開發人員使用視覺化設計工具,使用 MVVM 容易更順暢的設計工具 / Developer 工作流程的建立。因為檢視表只的任意的消費者在 ViewModel 它很容易就擷取一個檢視,從要呈現在 ViewModel 新檢視中的拖放。這個簡單的步驟可以讓快速建立原型和使用者介面設計工具所做的評估。

開發小組可以專注於建立穩固的 ViewModel 類別和設計小組可以著重在讓使用者易記的檢視中。連接兩個小組的輸出可能涉及一些多個確定正確的繫結存在檢視的 XAML 檔案中。

示範應用程式

這時候,我已檢閱 MVVM 的歷程記錄和作業的理論。我也會檢查原因所以常用中上 WPF 的開發人員。目前它是彙總您的發行中,並查看動作中的模式時間了。示範應用程式,本文所附會使用各種不同方式 MVVM。它提供許多來源的範例,說明將放入一個有意義的內容的概念。我可以在 Visual Studio 2008 SP1,Microsoft.NET Framework 3.5 SP1 建立示範應用程式。在 Visual Studio 單位測試系統中執行單元測試。

應用程式都可以包含任何數量的工作 」 區,「 每個使用者可以按一下開啟左邊巡覽區域中命令連結。所有的工作區存在於一個 TabControl 在主要內容區域中。使用者可以按一下 [關閉] 按鈕的工作區] 索引標籤的項目,以關閉工作區。應用程式有兩個可用的工作區: 所有客戶和新客戶。 在執行應用程式,並開啟某些工作區之後, UI 起來像 [圖 1

fig01.gif

[圖 1] 工作區

只有一個執行個體,所有客戶工作區的能在一段時間,但是任何數目的新客戶開啟工作區 」 可以開啟一次。當使用者決定建立新的客戶時,她必須填入 [圖 2 ] 中的資料輸入表單。

fig02.gif

[圖 2 新增客戶資料項目表單

填寫資料輸入表單以有效的值,並按一下 [儲存] 按鈕新客戶的名稱會出現在 [] 索引標籤之後項目和客戶會加入所有的客戶清單。應用程式並沒有刪除或編輯的現有的客戶支援,但該項功能和許多其他的功能類似它,可輕鬆實作建置現有的應用程式架構的頂端。既然您已經示範應用程式的用途,高階瞭解,讓我們來檢閱它已設計及實作。

轉送命令的邏輯

每個檢視,在應用程式將有您,以外的標準的現成程式碼類別的建構函式呼叫 InitializeComponent 空的程式碼後置檔案。事實上,您無法從專案移除檢視的程式碼後置檔案,應用程式仍會編譯並正確執行。雖然事件處理方法,在檢視中缺乏當使用者按一下按鈕,應用程式回應並滿足使用者的要求。這適用所建立的繫結有鑑於超連結]、 [按鈕和 [顯示在 UI 的 MenuItem 控制項的命令屬性。這些繫結,請確定當使用者按一下控制項上,ICommand,ViewModel 所公開的物件執行。您可以命令物件視為可讓您輕鬆使用一個 ViewModel 的功能,從檢視,在 XAML 中宣告的配接器。

在 ViewModel 會公開 I­command 的型別的執行個體屬性時, 命令物件就會通常使用 ViewModel 物件,取得完成其工作。一個可能的實作的模式是建立 ViewModel 類別,中的私用巢狀的類別,以便在命令具有私用成員,其包含 ViewModel 的存取權,並會不 pollute 命名空間。該巢狀的類別,實作 「 ICommand 介面,],並包含 ViewModel 物件的參考會插入至其建構函式。不過,建立巢狀的類別,實作一個 ViewModel 所公開的每個命令的 ICommand 可以 bloat ViewModel 類別的大小。更多碼表更可能的錯誤。

示範的應用程式 RelayCommand 類別後解決了這個問題。RelayCommand 可讓您插入命令的邏輯 (透過傳遞至其建構函式的委派。這個方法允許精簡、 簡潔的命令 ViewModel 類別實作。RelayCommand 是位於 [DelegateCommand 簡化的變化,Microsoft 的複合應用程式的程式庫. relay­command 類別如 [圖 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
}

CanExecuteChanged 事件 ICommand 介面實作中的一部分會具備一些項有趣的功能。 它會委派至 CommandManager.RequerySuggested 事件,事件訂閱。 這可確保 WPF 命令基礎結構要求所有 RelayCommand 物件,如果它們可以執行時,它會要求內建的命令。 自 CustomerViewModel 類別我會檢查下列的程式碼深入稍後,示範如何設定一個 RelayCommand 使用 Lambda 運算式:

RelayCommand _saveCommand;
public ICommand SaveCommand
{
    get
    {
        if (_saveCommand == null)
        {
            _saveCommand = new RelayCommand(param => this.Save(),
                param => this.CanSave );
        }
        return _saveCommand;
    }
}

ViewModel 類別階層架構

大部分的 ViewModel 類別都需要相同的功能。 它們通常需要實作 INotifyPropertyChanged 介面,它們通常需要有使用者易記顯示的名稱而且在工作區的情況下,它們需要能夠關閉 (也就是,從 UI 移除)。 這個問題自然提供本身來 ViewModel 基底類別或兩個的建立,讓新 ViewModel 類別可以的所有一般功能繼承自基底類別。 ViewModel 類別會形成繼承階層架構 圖 4 ] 所示。

fig04.gif

[圖 4 繼承階層架構

有基底類別,所有您 ViewModels 是的需求。 如果您想要取得您的類別中的功能一起,撰寫許多較小的類別,而非使用繼承 (Inheritance),這不是問題。 就像任何其他設計模式 MVVM 會是一指引,不規則組)。

ViewModelBase 類別

ViewModelBase 會是根類別階層架構就是它會實作常用的 INotifyPropertyChanged 介面,並具有 DisplayName 屬性的原因。 INotifyPropertyChanged 介面會包含呼叫 PropertyChanged 事件。 ViewModel 物件上的屬性會具有新值時, 它就可以引發 PropertyChanged 事件以通知新值的 WPF 繫結系統。 在接收該通知,時會繫結系統查詢屬性,並在繫結的屬性,一些 UI 項目上接收新的值。

為了瞭解 ViewModel 物件上的屬性已變更的 WPF PropertyChangedEventArgs 類別會公開型別的字串的 PropertyName 屬性。 您必須小心地將正確的屬性名稱傳遞至該事件引數,; 否則 WPF 會得到查詢錯誤的屬性,為新的值]。

ViewModelBase 一有趣的部分,是提供能夠確認 ViewModel 物件上實際上存在指定名稱屬性。 這非常有用時重整,因為變更屬性的名稱,透過 Visual Studio 2008 的重整功能將不會更新您的原始程式碼中包含該屬性的名稱會發生的字串 (也應該它)。 在事件引數可能會導致細微的錯誤因此這一點的功能可以是龐大的 timesaver 難以追蹤,是,則引發 PropertyChanged 事件與不正確的屬性名稱。 ViewModelBase 加入這個有用的支援程式碼如 [圖 5 ] 所示。

[圖 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);
    }
}

CommandViewModel 類別

CommandViewModel 最簡單的具體 ViewModelBase 子類別。 它會公開稱為命令 I­command 的型別的屬性。 MainWindowViewModel 會公開這些物件,透過其命令屬性的集合。 在主視窗的左邊,巡覽區域顯示連結每個 CommandViewModel,公開 MainWindowView­model,例如 」 檢視所有的客戶 」 和 「 建立新的客戶 」。 當使用者按一下連結時,因此執行其中一個這些的命令會工作區開啟主視窗上 TabControl。 command­ViewModel 類別 (Class) 定義如下所示:

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 檔案中有一個 data­template 其在於 CommandsTemplate 」。 MainWindow 會使用該範本,以呈現 CommandViewModels 先前所述的集合。 範本只會呈現為連結的 ItemsControl 的每個 CommandViewModel 物件。 每個超連結的命令屬性是繫結至一個 command­ViewModel 的命令屬性中。 [圖 6 ] 所示的 XAML。

[圖 6 呈現命令的清單

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

MainWindowViewModel 類別

為先前看到在類別圖表中 WorkspaceViewModel 類別會衍生自 ViewModelBase,並加入功能,以關閉中。 「 關閉 」 的意思項目移除工作區使用者介面在執行階段。 三個的類別衍生自 WorkspaceViewModel: MainWindowViewModel,AllCustomersViewModel,和 CustomerViewModel。 MainWindowViewModel 的要求,以關閉是由應用程式類別,建立如 [圖 7 ] 所示的 [MainWindow 和其 ViewModel,處理。

[圖 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 將包含您,功能表項目的命令屬性繫結至 MainWindowViewModel 的 CloseCommand 屬性。 使用者按一下該功能表項目上時,應用程式類別回應,藉由呼叫視窗的關閉方法,會像這樣:

<!-- 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 物件的可觀察到集合。 在主視窗會包含在 TabControl 的 ItemsSource 屬性繫結至該集合。 每個的索引標籤項目都有其的命令屬性繫結至其對應的 WorkspaceViewModel 執行個體的 [CloseCommand [關閉] 按鈕。 abridged 的版本,設定每個索引標籤的項目範本的顯示,如下的程式碼中。 程式碼位於 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>

當使用者按一下 workspace­ViewModel 的 CloseCommand 執行,讓其 request­Close 事件引發一個索引標籤項目中的 [關閉] 按鈕。 MainWindowViewModel 會監視其工作區的 RequestClose 事件,並要求時,工作區 」 集合中移除工作區。 由於 main­window 的 TabControl) 可觀察到的 WorkspaceViewModels 集合繫結其 ItemsSource 屬性,從集合中移除的項目會導致 TabControl 從中移除對應的工作區。 main­WindowViewModel 從該邏輯如 [圖 8 ] 所示。

從 UI 移除工作區] 的 [圖 8

// 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 模式,大 selling 點,因為它允許進行測試而不需要撰寫接觸到 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.");
}

將資料檢視套用一個 ViewModel

MainWindowViewModel 會間接加入,並移除 workspace­ViewModel 物件從主視窗的 tab­control。 由資料繫結所依賴,一個 TabItem,內容屬性會接收顯示 ViewModelBase-衍生物件。 ViewModelBase 不是 UI 項目,因此它具有呈現本身不繼承支援。 根據預設,在 WPF 非 Visual 物件會呈現藉由在 TextBlock 中顯示它的 ToString 方法呼叫的結果。 清楚地是不哪些您需要除非您的使用者燒錄想要看到我們 ViewModel 類別的型別名稱 !

您可以輕鬆地告訴 WPF 如何呈現為 ViewModel 物件使用具型別 DataTemplate 的動作。 具型別的 DataTemplate 並沒有指派給它的的 x: Key] 值,但它也會將它的資料型別屬性,設定為型別類別的執行個體。 如果 WPF 會嘗試呈現的其中一個您 ViewModel 物件,它會檢查,請參閱如果資源) 系統具有型別的 DataTemplate 範圍中其資料型別是相同 (或基底類別的) 您 ViewModel 物件的型別。 如果有找到,會使用該範本] 索引標籤項目的內容屬性所參考 ViewModel 物件的呈現。

MainWindowResources.xaml 檔案會有一個 resource­dictionary。 該字典會加入至主視窗的資源階層表示,它包含的資源在視窗的資源範圍中。 當索引標籤項目的內容設定為一個 ViewModel 物件時,在具型別的 DataTemplate,從這個字典提供檢視 (也就是,使用者控制項) 來呈現它,如 [圖 10 ] 所示。

[圖 10] 提供檢視

<!-- 
This resource dictionary is used by the MainWindow. 
-->
<ResourceDictionary
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://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>

您不需要撰寫任何程式碼所決定的檢視,顯示一個 ViewModel 物件。 WPF 的資源系統將會所有粗) 工作您釋放您到專注於更重要的事項。 在 [更複雜的案例則可能要以程式設計方式選取檢視,但在大部分是不必要的情況下。

資料模型和存放庫

您已瞭解如何載入、 顯示,和關閉應用程式殼層 ViewModel 物件。 現在,一般的配管在位置,您可以檢閱實作更特定的應用程式網域的詳細資訊。 深層到應用程式的兩個工作區,所有客戶取得之前和 「 新增客戶 」 讓我們先檢查資料模型和資料存取類別。 這些類別的設計毫無幾乎使用 MVVM 的模式因為您可以建立 ViewModel 類別來調整幾乎任何資料物件到 WPF 到易記的東西。

示範程式中的的唯一的模型] 類別會為客戶。 該類別將具有您,少數的屬性,表示公司例如其第一個名稱]、 [上次登入的名稱和 [電子郵件地址的客戶有關的資訊。 它是藉由實作標準年 WPF 叫用,街道之前存在的 IDataErrorInfo 介面提供驗證訊息。 客戶類別會有任何建議的 MVVM 架構中,或甚至在 WPF 應用程式正在使用中。 類別無法輕易地有來自舊版的企業程式庫中。

資料必須來自,而且位於某處。 在此的應用程式中 CustomerRepository 類別的執行個體會載入,並儲存所有的 Customer 物件中。 發生載入客戶資料,從 XML 檔案,但不相關的外部資料來源類型。 資料可能來自資料庫、 Web 服務、 具名的管道、 磁碟或甚至通信業者 pigeons 資料檔案: 它只是並不重要。 只要您有在.NET 物件的某些資料無論它來自何處,MVVM 模式就可以取得那個資料在螢幕上。

CustomerRepository 類別公開可讓您取得所有可用的 Customer 物件的一些方法將新客戶的存放庫,並檢查客戶是否已經在儲存機制中。 因為應用程式不允許使用者刪除客戶,存放庫不允許您要移除的客戶。 CustomerAdded 事件引發時使用新的客戶會進入 [CustomerRepository,透過 AddCustomer 方法。

清楚地,這個應用程式的資料模型是非常小相較於應用哪些實際商務程式需要,但是,並無法很重要。 一定要了解是如何 ViewModel 類別會讓客戶和 CustomerRepository 使用。 請注意,customer­ViewModel 是 Customer 物件包裝。 它會公開一個的客戶的狀態] 和 [customer­view 控制項透過一組屬性所使用的其他狀態。 CustomerViewModel 不重複的客戶的狀態 ; 它會它只公開透過委派,像這樣:

public string FirstName
{
    get { return _customer.FirstName; }
    set
    {
        if (value == _customer.FirstName)
            return;
        _customer.FirstName = value;
        base.OnPropertyChanged("FirstName");
    }
}

當使用者建立新的客戶,並按一下 [儲存] 按鈕,在 CustomerView 控制項時,在 Customer­ViewModel 關聯檢視會將新客戶物件加入至 customer­repository。會,導致儲存機制的 CustomerAdded 事件引發,讓 [AllCustomers­ViewModel 知道它應該將新的 Customer­ViewModel 加入至其 AllCustomers 集合。在的觀念中 Customer­repository 做為處理客戶物件的各種 ViewModels 之間同步處理機制。可能是其中一個可能視為這使用 mediator 設計模式。我將會檢閱這個方式在之即將推出的區段中,但現在的多個參考到 [圖 11 ] 中圖表的高階的瞭解的方式的所有片段,都搭配。

fig11.gif

[圖 11 客戶的關聯性

新的客戶資料項目表單

當使用者中,按一下 [建立新的客戶] 連結時,請 MainWindowViewModel 則會將新的 CustomerViewModel 加入至其的清單的工作區,並 CustomerView 控制項顯示。使用者輸入有效的值,請在 [輸入] 欄位之後,[儲存] 按鈕就會進入啟用的狀態,讓使用者可以保存新的客戶資訊。沒有任何項目以下的一般,只是一般資料輸入表單輸入驗證並 [儲存] 按鈕。

客戶類別會有內建支援,可透過其 IDataErrorInfo 介面實作的驗證。驗證可確保客戶擁有第一個名稱語式正確 (Well-Formed) 的電子郵件地址,且客戶為使用者,最後一個名稱。如果客戶的 IsCompany 屬性傳回 true,[姓氏] 屬性不能有值 (概念的公司沒有最後一個名稱)。此驗證邏輯可能會使觀點客戶物件的意義,但不符合需求的使用者介面]。UI 會要求使用者選取新的客戶是否使用者或公司。在客戶類型選取器一開始會有 「 (未指定) 」 值。要如何才能在 UI 知道使用者客戶類型是未指定,是否為 True 或 False 值只允許客戶在 IsCompany 屬性?

假設您可以在整個軟體系統的完全控制,您無法變更 IsCompany 屬性型別可為 Null <bool>,它會允許未選取這個 」 值。不過,真實情況不一定那麼簡單。假設您無法變更 Customer 類別,因為它是來自舊版程式庫,您的公司在不同小組所擁有。如果有而該怎麼辦不容易方式保存未選取這個 」 值因為現有的資料庫結構描述?如果其他應用程式已經使用客戶類別,並依賴的一般的布林值屬性呢?再次,有一個 ViewModel 是以 Rescue。

在測試方法,在 [圖 12] 會顯示這項功能如何在 CustomerViewModel。CustomerViewModel 公開 CustomerTypeOptions 屬性,以便客戶類型選取器有三個要顯示的字串。它也會公開 CustomerType 屬性,在 Selector 中儲存選取的字串。設定 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 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}"
  />

已變更的下拉式方塊中選取的項目時, 在資料來源的 IDataErrorInfo 介面查詢,以查看新的值是否有效。 發生 SelectedItem 屬性繫結已 ValidatesOnDataErrors 的設定,為 true。 因為資料來源為 customer­ViewModel 物件繫結系統要求的 customer­ViewModel 驗證錯誤 CustomerType 屬性上。 大部分時間,CustomerViewModel 會委派至客戶物件,它包含驗證錯誤的所有要求。 不過,因為客戶沒有概念,讓 IsCompany 屬性的未選取的狀態的 CustomerViewModel) 類別必須處理驗證新 ComboBox 控制項中選取的項目。 [圖 14 ] 中,會出現該程式碼。

[圖 14] 驗證 CustomerViewModel Object

// 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";
}

這個程式碼的金鑰的外觀會是 IDataErrorInfo CustomerViewModel 的實作可以處理 ViewModel-特定屬性驗證的要求,並委派其他要求至客戶物件。 這可讓您使用的驗證邏輯模型的類別和有額外的驗證,只能利用 ViewModel 類別的屬性。

能夠儲存在 CustomerViewModel 是透過 SaveCommand 屬性檢視可用的。 該命令會使用 RelayCommand 類別之前檢查,以允許 CustomerViewModel 決定如果它可以儲存本身以及要告訴以儲存其狀態時。 在這個的應用程式儲存新的客戶只表示一個 CustomerRepository 將它加入。 決定新的客戶是否已準備要儲存需要雙方同意。 客戶物件必須是要求它是否有效,或不,並在 customer­ViewModel 必須決定它是否有效。 此兩個部分的決策是必要,因為 ViewModel-特定的屬性和檢查先前的驗證。 Customer­ViewModel 儲存邏輯如 [圖 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 物件,並讓布林 (Boolean) 屬性的未選取這個 「 狀態等。 它也會提供能夠輕易地通知客戶,以儲存其狀態。 如果檢視表直接繫結至 Customer 物件,檢視將會需要大量的適當地進行這項工作的程式碼。 在設計良好的 MVVM 架構中大部分檢視的程式碼後置應該空,或者,最多只能包含操作控制項和包含在該檢視的資源的程式碼。 有時候也需要在與 ViewModel 物件例如攔截事件互動的檢視表的程式碼後置中撰寫程式碼或呼叫方法,會否則會非常困難要從 [ViewModel 本身叫用。

所有的客戶檢視

示範應用程式也會包含 ListView 中會顯示所有客戶的工作區。 在清單中的,客戶會分組根據它們是否公司或使用者。 使用者可以在時間中選取 [一或多個客戶,並檢視其總銷售額的總和,底端右上角中。

UI 會是 AllCustomersView 控制項的呈現 AllCustomersViewModel 物件。 每個 ListView­item 將表示您,CustomerViewModel AllCustomerViewModel 物件所公開 (Expose) AllCustomers 集合中的物件。 在 [舊] 區段中中, 您所見,如何在 CustomerViewModel 可呈現為資料輸入表單,並完全相同的 CustomerViewModel 物件現在呈現為 ListView 中的項目。 CustomerViewModel 類別會有不知道哪些 Visual 項目顯示,這就是為何這種重新使用是可能的原因。

AllCustomersView 會建立群組出現在清單檢視。 它? 詫完成繫結設定像 [圖 16 為 collection­ViewSource ListView 的 ItemsSource

[圖 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 屬性建立的。 樣式指定給該屬性套用至每個 ListViewItem 讓 ListViewItem,以在 CustomerViewModel 上繫結至屬性上的屬性。 該樣式中一個重要的繫結會建立一個 ListViewItem,IsSelected 屬性] 和 [customer­ViewModel 這裡看到的 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 類別負責維護該值,在 ContentPresenter 下方清單檢視可以顯示正確的號碼。 [圖 17] 會顯示如何 AllCustomersViewModel 監視每個客戶所選取或取消選取] 和 [通知它需要更新顯示值的檢視。

[圖 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 物件無法套用貨幣格式,檢視的 」 而不是藉由從 TotalSelectedSales 屬性傳回字串而不是雙精度浮點數值。 已加入 ContentPresenter 的 [ContentStringFormat] 屬性,在.NET Framework 3.5 SP1 中,所以如果您必須為目標的 WPF 舊版本,您必須套用貨幣格式程式碼中:

<!-- In AllCustomersView.xaml -->
<StackPanel Orientation="Horizontal">
  <TextBlock Text="Total selected sales: " />
  <ContentPresenter
    Content="{Binding Path=TotalSelectedSales}"
    ContentStringFormat="c"
  />
</StackPanel>

向上換行

WPF 會有許多提供應用程式開發人員,並且學習使用的電源需要一個思維模式移位]。模型-檢視-ViewModel 模式是簡單且有效一套設計和實作一個 WPF 應用程式的方針。它可讓您建立強式分隔資料、 行為和方便控制,是軟體開發的混亂的簡報。

我想要感謝 John Gossman 他協助本篇文章。

Josh Smith 是熱情有關使用 WPF 建立絕佳的使用者經驗。他是他的工作在 WPF 社群中,授予 Microsoft MVP) 標題。Josh 適用 Infragistics 經驗設計群組中。當他在電腦進行不時,他樂於播放的鋼琴讀取有關歷程記錄和探索紐約的城市,與他 girlfriend。您可以造訪 Josh 的部落格,在joshsmithonwpf.wordpress.com.