パターン - MVVM デザイン パターンを使用した WPF プログラム開発

説明する

この記事は、Josh Smith が MSDN で発行したジャーナル「Patterns-WPF Apps With The Model-View-ViewModel Design Pattern」を著者が独自に翻訳したものです。これは純粋に作者の個人的な好みです。侵害がある場合は、関係者に知らせてください。また、著者のエネルギー、経験、レベルには限界があり、不適切な翻訳も多々ありますので、皆様のご批判、ご指摘をいただければ幸いです。
記事内のデモ コードの作成者は中国語の説明書にも注釈を付けており、具体的なソース コード ファイルは作成者が整理した後、この記事で更新されます。

文章

プログラム開発の「猛獣」を飼いならすために使用できる一般的なデザイン パターンは数多くありますが、そのほとんどを合理的に分離して解決することは非常に困難です。デザイン パターンが複雑になればなるほど、これまでショートカットを使用して行っていた作業の多くが置き換えられる可能性が高くなります。
これは必ずしもデザイン パターンのせいではありません。使用される UI プラットフォームが一部の単純なデザイン パターンに適していない可能性があるため、複雑なデザイン パターンを使用するために大量のコードを記述する必要がある場合があります。したがって、本当に必要なのは、UI を作成するための、イベントでテストされ、開発者が承認したシンプルなデザイン パターンです。幸いなことに、WPF (Windows Presentation Foundation) はまさに必要なデザイン パターンです。
ソフトウェアの世界で WPF の使用が増加するにつれて、WPF コミュニティは開発モデルとアプリケーションの実践に関する独自のエコロジーを確立しました。この記事では、WPF でクライアント アプリケーションを設計および実装するためのベスト プラクティスについて説明します。WPF のいくつかの機能と MVVM デザイン パターンを組み合わせて、WPF アプリケーションを簡単かつ正確に作成する方法をサンプル プログラムを使用して説明します。
この記事の最後では、データ テンプレート、コマンド、データ バインディング、リソース システム、MVVM パターンを組み合わせて、シンプルでテスト可能で安定したフレームワーク WPF アプリケーションを作成する方法を説明します。この記事で実装したデモ プログラムは、実際に MVVM デザイン パターンを使用した WPF アプリケーション テンプレートとして使用できます。デモ シナリオでの単体テストの使用は、ビジネス ロジックを ViewModel クラスに分離した後、アプリケーションのユーザー インターフェイスをテストすることがいかに簡単であるかを示しています。詳細に入る前に、そもそも MVVM 設計パターンの使用を検討する必要がある理由を確認してみましょう。

秩序 vs カオス

単純な「Hello, World!」プロジェクトではデザイン テンプレートを使用する必要はありません。有能な開発者であれば、これらの数行のコードを一目で読むことができます。ただし、プログラム内の機能の数が増えると、コードの量も増加し、対応するユーザー コントロールも増加します。最終的に、反復的なコードと複雑なシステムの両方が、開発者に、理解しやすく、伝達し、拡張し、問題を解決しやすい方向にコードを最適化する動機を与えます。標準名 (システム内でコードが果たす役割によって定義される) を使用してソース コード内の特定のエンティティに名前を付けることで、システム内での識別の混乱を軽減します。
開発者は、実装メソッドの階層を持たせるために、設計テンプレートに従って意図的にコードを構造化することがよくありますが、このアプローチには何の問題もありません。ただし、この記事では、MVVM で特定のクラス名を定義するための標準用語の使用など、MVVM を WPF アプリケーション フレームワークとして使用する利点を検証しました。たとえば、クラスが View の抽象クラスの場合、クラス名は「ViewModel」で終わります。 "終わり。この方法により、前述の認識の混乱の問題を回避できます。代わりに、ほとんどのソフトウェア開発プロジェクトに蔓延する、制御可能な混乱のようなものに満足することができます。

MVVMの進化

ユーザー対話のソフトウェア設計により、多くのよく知られた設計パターンが生まれました。たとえば、MVP (Model-View-Presenter) パターンは、さまざまな UI ソフトウェア設計プラットフォームで非常に人気があります。MVP は、数十年にわたって開発されてきた MVC (Model-View-Controller) パターンのバリアントです。MVP パターンを使用したことがない場合のために、ここで簡単に説明します。画面の前面に表示されるものはビュー、そこに表示されるデータはモデル、そしてプレゼンターはその 2 つを接続します。ビューはプレゼンターに依存して、モデルにデータを設定し、ユーザー入力に反応し、入力検証を提供します (おそらく委任を介してモデルに送信されます)。その他の同様のタスクも実行します。MVP についてさらに詳しく知りたい場合は、Jean-Paul Boodhoo の2006 年 8 月のデザイン パターン コラムを読むことをお勧めします。
2004 年に、Martin Fowler は PM (プレゼンテーション モデル) と呼ばれるパターンに関する記事を発表しました。PM パターンは、MVP パターンと同様に、ビューを動作や状態から分離します。PM パターンの興味深い部分は、プレゼンテーション モデルと呼ばれる抽象ビューの作成です。ビューはプレゼンテーション モデルのレンダリングにもなります。ファウラー氏の説明では、プレゼンテーション モデルは 2 つの間の同期を確保するために頻繁にビューを更新する必要があると説明しました。同期ロジック コードは、プレゼンテーション モデル クラスに記述されます。
2005 年に、John Gossman (現在は Microsoft の WPF および Silverlight アーキテクト) が MVVM パターンをブログで公開しました。MVVM は Fowler によって提案されたプレゼンテーション モデルに相当し、両モードの特徴はビュー ステートと動作を含む抽象化です。Fowler は、UI プラットフォームに依存しない抽象化を作成するためのビューとしてプレゼンテーション モデルを紹介し、Gossman は、ユーザー インターフェイスのコア機能を簡素化する標準化された方法として MVVM を紹介しました。この観点から、私は MVVM を、特に WPF および Silverlight プラットフォーム向けに調整された、より一般的な PM パターンだと考えています。
Glenn Block は、2008 年 9 月号に優れた記事「Prism: Patterns for Building Composite Applications with WPF」を発表しました。彼は、Microsoft の WPF 用複合アプリケーション ガイドラインについて説明しています。ViewModel という用語は決して使用されず、代わりに、View の抽象化を説明するためにプレゼンテーション モデルという用語が使用されます。ただし、本文全体を通じて、私は MVVM パターンを好み、抽象ビューを ViewModel として使用します。この用語は WPF および Silverl コミュニティでより人気があることがわかったからです。
MVP のプレゼンターとは異なり、ViewModel はビューへの参照を実装する必要がありません。View はプロパティを ViewModel にバインドし、逆に ViewModel は Model オブジェクトと View 固有の状態を含むプロパティを公開します。View と ViewModel の間のバインディングの構築は非常に簡単で、ViewModel オブジェクトを View のコンテキスト (DataContext) として設定するだけです。ViewModel 内のプロパティの値が変更された場合、新しい値はバインディングを通じて View に自動的に渡されます。ユーザーがビュー内のボタンをクリックすると、ViewModel 内のコマンドが対応するリクエストを実行します。ViewModel であっても View であっても、Model データに対するすべての変更が実行されます。
View クラスは Model クラスの存在を知りません。また、ViewModel と Model は View について何も知りません。実際、Model は ViewModel と View の存在を明らかに知っています。これは非常に疎結合な設計であり、その利点については次に説明します。

WPF 開発者が MVVM を好む理由

開発者が WPF と MVVM に精通すると、2 つを区別するのが難しくなります。MVVM は WPF プラットフォームによく適しており、(他のプラットフォームと比較して) MVVM を使用したアプリケーションの作成が容易になるように設計されているため、WPF 開発者の共通語です。実際、Microsoft も MVVM を使用して Microsoft Express Blend などの WPF アプリケーションを社内で開発しており、コアとなる WPF プラットフォームも構築中です。MVVM は、緩やかな制御モデルとデータ テンプレート、状態と動作の強力な分離など、WPF の多くの側面を強調しています。
MVVM を WPF の優れたパターンにする最も重要な点は、データ バインディング インフラストラクチャの使用です。View のプロパティを ViewModel にバインドすることで、両者の間の結合が軽減され、ViewModel にコードを記述して View を直接更新することで、View が完全に回避されます。データ バインディング システムは入力検証もサポートしており、入力検証エラー メッセージをビューに渡す標準的な方法を提供します。
このパターンを WPF でうまく機能させるもう 2 つの点は、データ テンプレートとリソース システムの使用です。データ テンプレートは、ユーザー インターフェイスに ViewModel オブジェクトを表示するビューで使用されます。XAML でテンプレートを宣言すると、リソース システムが実行時にそれらのテンプレートを自動的に見つけて適用することができます。バインディングとデータ テンプレートの詳細については、2008 年 7 月の記事「データと WPF: データ バインディングと WPF を使用してデータ表示をカスタマイズする」を参照してください。
WPF がコマンドをサポートしていない場合、MVVM パターンはそれほど強力ではありません。この記事では、ViewModel が View にコマンドを公開する方法、つまり View が ViewModel の機能をどのように使用できるかを説明します。コマンドにあまり詳しくない場合は、2008 年 9 月に Brain Noyes によって発行された包括的な記事「Advanced WPF: Understanding Routed Events and Commands in WPF」を読むことをお勧めします。
MVVM は、WPF と Silverlight2 の自然なアプリケーション構築方法であるという特徴に加えて、単体テスト (uint テスト) で ViewModel クラスを非常に簡単に使用できることでも有名です。アプリケーションの内部ロジックが ViewModel クラスのセットに格納されている場合、それをテストするコードを簡単に作成できます。ある意味、ビューと単体テストは 2 つの異なるタイプの ViewModel コンシューマーです。ViewModel の一連のテスト メソッドは、アプリケーションに無料で高速な回帰テスト機能を提供し、アプリケーション時間の維持コストの削減に役立ちます。
自動回帰テストの作成機能の向上に加えて、ViewModel クラスのテスト容易性により、開発者はユーザー インターフェイスの外観をより簡単に設計できます。アプリケーションを設計するときは、通常、コンテンツが View に配置されるか ViewModel に配置されるかを判断するための ViewModel の単体テストを作成することを検討できます。UI オブジェクトを作成せずに ViewModel の単体テストを作成できれば、特定の視覚要素から独立して ViewModel の外観を完全に設計できます。
最後に、ビジュアル デザイナーを使用する開発者にとって、MVVM を使用すると、スムーズなデザイナー/開発者のワークフローを簡単に作成できます。View は任意の ViewModel のユーザーになれるため、別の View を置き換えて ViewModel をレンダリングすることは非常に簡単です。これにより、設計者はアプリケーションのユーザー インタラクションを迅速に構築して評価できるようになります。
開発チームは安定した ViewModel クラスの作成に集中でき、設計チームはユーザー フレンドリーなビューの実装に集中できます。ビューの XAML ファイルに正しいバインディングが実装されていることを確認するだけで、2 つのチーム間で出力が一致することが保証されます。

デモアプリケーション

ここまで、MVVM の歴史と動作原理を振り返り、MVVM が WPF 開発者の間で非常に人気になっている理由を説明してきました。今こそ、腕まくりをして実際の作業に取り掛かるときです。この記事のデモ例では、MVVM を適用するさまざまな方法が使用されています。コンセプトを現実に変えるのに役立つ豊富なサンプル リソースが提供されています。デモアプリケーションはVisual Studio 2015 SP1とMicrosoft .NET Framework 5.1 SP1の環境で作成しました。Visual Studio の単体テスト システムで単体テストを実行します。
アプリケーションには任意の数のワークスペース (ワークスペース) が含まれており、ユーザーは左側のナビゲーション バーのコマンド リンクをクリックしてワークスペースの 1 つを開きます。すべてのワークスペースは、メイン コンテンツ表示領域のタブ コントロール (TabControl) に保存されます。ユーザーは、ワークスペース タブの閉じるボタンをクリックしてタブを閉じることができます。アプリには、「すべてのアカウントを表示」と「新しいアカウントを作成」という 2 つの使用可能なワークスペースが含まれています。アプリケーションを実行していくつかのワークスペースを開くと、ユーザー インターフェイスは図 1 のようになります。
図 1 ワークスペース
アプリケーション内の「すべての顧客の表示」ワークスペースのインスタンスは 1 回だけ開くことができますが、「新規顧客の作成」ワークスペースはいくつでも開くことができます。ユーザーが新しい顧客を作成する場合は、図 2 のフォームに記入する必要があります。
図 2 新しい顧客フォームの作成
フォームに有効な値を入力して「保存」ボタンをクリックすると、新規顧客の名前がタブに表示され、新規顧客がすべての顧客のリストに追加されます。このアプリケーションは既存の顧客の削除と編集をサポートしていませんが、この機能および他の同様の機能は既存のアプリケーション フレームワーク上に実装できます。デモ アプリケーションについては十分に理解できたはずです。次に、デモ アプリケーションがどのように設計され、実装されたかを調べてみましょう。

依存するコマンドロジック

コンストラクターで生成される InitializeComponent と呼ばれる標準サンプル コードを除き、このアプリケーションのすべてのビューの背後に空のコード ファイルがあります。実際、プロジェクトから View のバックグラウンド コードを削除しても、アプリケーションは引き続きコンパイルして通常どおり実行できます。ビューにはイベント処理メソッドがないにもかかわらず、ユーザーがボタンをクリックすると、アプリケーションはユーザーの要求を満たすために応答できます。これが可能な理由は、ユーザー インターフェイスのハイパーリンク、ボタン、メニュー バーの名前付けプロパティに組み込まれたバインド メカニズムのためです。これらのバインディングにより、ユーザーがこれらのコントロールをクリックすると、ViewModel によって公開される ICommand オブジェクトが実行されます。コマンド オブジェクトは、XAML で宣言された ViewModel の関数を View から簡単に使用できるようにするアダプターと考えることができます。
ViewModel が ICommand 型のインスタンス プロパティを公開すると、コマンド オブジェクトは ViewModel オブジェクトを使用してその作業を実行します。考えられる実装パターンの 1 つは、ViewModel クラス内にプライベートの入れ子になったクラスを作成して、名前空間に影響を与えることなく、そこに含まれる ViewModel のプライベート メンバーにコマンドがアクセスできるようにすることです。この入れ子になったクラスは ICommand インターフェイスを実装し、コンストラクターで含まれる ViewModel オブジェクトへの参照を宣言します。ただし、ViewModel で公開される各コマンドに対して ICommand を実装する埋め込みクラスを作成すると、ViewModel のサイズが増加します。コードが増えると、バグが発生する可能性が高くなります。
デモ アプリケーションでは、RelayCommand クラスを使用して上記の問題を解決します。RelayCommand を使用すると、コンストラクターでデリゲートを渡してコマンドのロジックを実装できます。このメソッドは、ViewModel クラスの簡潔で明確なコマンドを使用して実装できます。RelayCommand は、Microsoft 複合アプリケーション ライブラリの DelegateCommand の単純なバリアントです。RelayCommand は、以下の図 3 に示すように実装されます。

public class RelayCommand : ICommand
{
    
    
    #region Fields 
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;
    #endregion // 字段 
    #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 // 构造函数 
    #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 成员 
}

Icommand インターフェイスの一部として実装された CanExecuteChanged イベントには、いくつかの興味深いプロパティがあります。これは、デリゲートを介して CommandManager.ReuqerySuggested イベントをサブスクライブします。これにより、WPF コマンド フレームワークがすべての RelayCommadn オブジェクトに内部コマンドを実行できるかどうかを確認するようになります。次のコードは CustomerViewModel クラスからのもので、ラムダ式を使用して RelayCommand を構成する方法を示しています。後で詳しく分析します。

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

ViewModel クラス階層

ほとんどの ViewModel クラスは、いくつかの共通の機能を共有します。通常、INotifyPropertyChanged インターフェイスを実装する必要があり、この例のワークスペースでは、わかりやすい表示名を持つ必要があり、ウィンドウの機能を閉じる必要もあります。この問題は当然ながら、新しく作成された ViewModel クラスが基本クラスの共通関数を継承できるようにするための ViewModel の 1 つまたは 2 つの基本クラスの作成に関連しています。ViewModel クラス フォームの継承階層を図 4 に示します。
図 4 継承階層
作成するViewModelの基本クラスを作成する必要があります。クラス内で多数の小さなクラスを組み合わせて機能を実装する傾向がある場合は、階層を使用しなくても問題ありません。他のデザイン パターンと同様、MVVM はガイドラインであり、ルールではありません。

ViewModelBase クラス

ViewModelBase は階層内のルート クラスであり、共通の INotifyPropertyChanged インターフェイスを実装し、DisplayName プロパティを持っています。INotifyPropertyChanged インターフェイスには、PropertyChanged というイベントが含まれています。ViewModel オブジェクトのプロパティに新しい値があると、PropertyChanged イベントが発生して、新しい値があることを WPF のバインディング システムに通知します。通知を受信すると、バインディング システムはこのプロパティのクエリを開始し、このプロパティにバインドされた UI 要素は新しい値を取得します。
WPF が ViewModel オブジェクトのどのプロパティが変更されたかを認識するために、PropertyChangedEventArgs クラスは String 型の PropertyName プロパティを公開します。正しいプロパティ名をプロパティ パラメーターに渡すように細心の注意を払う必要があります。そうしないと、WPF は間違ったプロパティ値をクエリします。
ViewModelBase の興味深い点は、指定された名前のプロパティが ViewModel オブジェクトに実際に存在することを検証する機能を提供していることです。Visual Studio 2008 のリファクタリング機能でプロパティ名を変更しても、ソース コード内のプロパティ名を含む文字列は更新されないため、この機能はリファクタリング時に非常に役立ちます。イベント パラメーターに間違ったプロパティ名を指定して PropertyChanged イベントをトリガーすると、追跡が難しい非常に微妙なバグが発生するため、この検証機能によりトラブルシューティングにかかる​​時間を大幅に節約できます。この便利な機能を追加する 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);
    }
}

CommandViewModel クラス

最も単純な具象 ViewModelBase サブクラスは CommandViewModel です。これは、Command という名前の ICommand 型のプロパティを公開します。MainWindowViewModel は、command プロパティを通じて CommandViewModel のコレクションを公開します。メイン ウィンドウの左側のナビゲーション バー領域には、「すべての顧客の表示」や「新しい顧客の作成」など、MainWindowViewModel によって公開される各 CommandViewModel へのリンクが表示されます。ユーザーがリンクの 1 つをクリックしてコマンドの 1 つを実行すると、メイン ウィンドウのタブにワークスペースが開きます。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 ファイルには、キー「CommandsTemplate」を持つデータ テンプレートがあります。メイン ウィンドウは、このテンプレートを使用して、前述の CommandViewModel のコレクションをレンダリングします。このテンプレートは、単純に CommandViewModel オブジェクトを ItemsControl 内のリンクとしてレンダリングします。各ハイパーリンクの Command プロパティは、CommandViewModel の Command プロパティにバインドされます。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 の 3 つのクラスを派生します。MainWindowViewModel のクローズ要求は、以下の図 7 に示すように、MainWindow とその ViewModel を作成した App クラスによって処理されます。

// In App.xaml.cs 
protected override void OnStartup(StartupEventArgs e)
{
    
    
    base.OnStartup(e); MainWindow window = new MainWindow();
    // 创建主窗口绑定的ViewModel
    string path = "Data/customers.xml";
    var viewModel = new MainWindowViewModel(path);
    // 当ViewModel被询求关闭,则关闭窗口 
    viewModel.RequestClose += delegate {
    
     window.Close(); };
    // 通过设置上下文遍历元素树,将窗口中所有的控件绑定到ViewModel
    window.DataContext = viewModel;
    window.Show();
}

MianWindow にはメニュー列が含まれており、メニューの Command プロパティは MainWindowViewModel の CloseCommand プロパティにバインドされています。ユーザーがメニューのこの列をクリックすると、次のようにウィンドウの Close メソッドを呼び出すことによって App クラスの応答がトリガーされます。

<!-- In MainWindow.xaml -->
<Menu>
    <MenuItem Header="_文件">
        <MenuItem Header="_退出" 
                    Command="{Binding Path=CloseCommand}" />
    </MenuItem>
    <MenuItem Header="_编辑" />
    <MenuItem Header="_设置" />
    <MenuItem Header="_帮助" />
</Menu>

MainWindowViewModel には、Workspaces と呼ばれる WorkspaceViewModel オブジェクトのビジュアル コレクションが含まれています。メイン ウィンドウには、ItemsSource プロパティがこのコレクションにバインドされているタブ コントロールが含まれています。各タブには閉じるボタンがあり、その Command プロパティは、関連付けられた WorkspaceViewModel インスタンスの CloseCommand にバインドされています。各タブを構成するテンプレートの短縮コードを以下に示します。コードは MainWindowResource.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>

ユーザーがタブの閉じるボタンをクリックすると、WorkspaceViewModel の CloseCommand が実行され、RequestClose イベントがトリガーされます。MainWindowViewModel はワークスペースの RequestClose イベントを監視し、イベントが発生すると、対応するワークスペースが Workspaces コレクションから削除されます。メイン ウィンドウのタブ コントロールの ItemsSource プロパティは WorkspaceViewModel のこのコレクションにバインドされているため、コレクションから項目を削除すると、対応するワークスペースがタブ コントロールから削除されます。MianWindowViewModel のロジックを以下の図 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 クラスの単体テストを簡単に作成できることは、UI 関連のコードを記述せずに簡単なプログラム テストを可能にするため、MVVM 設計パターンの大きなセールス ポイントです。このテスト方法を以下の図 9 に示します。

// In MainWindowViewModelTests.cs 
[TestMethod]
public void TestCloseAllCustomersWorkspace()
{
    
    
    // 创建的是MainWindowViewModel,不是MainWindow. 
    MainWindowViewModel target = new MainWindowViewModel(Constants.CUSTOMER_DATA_FILE);
    Assert.AreEqual(0, target.Workspaces.Count, "Workspaces isn't empty.");
    // 查找打开“查看所有客户”工作空间的命令. 
    CommandViewModel commandVM = target.Commands.First(cvm => cvm.DisplayName == "View all customers");
    // 打开“查看所有客户”的工作空间. 
    commandVM.Command.Execute(null); Assert.AreEqual(1, target.Workspaces.Count, "Did not create viewmodel.");
    // 确保创建的工作空间的格式正确. 
    var allCustomersVM = target.Workspaces[0] as AllCustomersViewModel;
    Assert.IsNotNull(allCustomersVM, "Wrong viewmodel type created.");
    // 通知“查看所有客户”的工作空间关闭. 
    allCustomersVM.CloseCommand.Execute(null);
    Assert.AreEqual(0, target.Workspaces.Count, "Did not close viewmodel.");
}

ビューを ViewModel に適用する

MainWindowViewModel は、WorkspaceViewModel オブジェクトに対するメイン ウィンドウのタブ項目の追加と削除を間接的に実装します。データ バインディングに依存して、タブの Content プロパティは ViewModelBase に依存するオブジェクトを受け取り、表示します。ViewModelBase は UI 要素ではないため、内部的には独自のレンダリングをサポートしません。既定では、WPF は、テキスト ブロック (TextBlock) 内の非ビジュアル オブジェクトの ToString メソッドを呼び出してオブジェクトを表示することをサポートしています。ユーザーが ViewModel 型の型名を見たがっている場合を除き、これは明らかに必要なものではありません。
型指定された DataTemplate を使用すると、WPF に ViewModel オブジェクトのレンダリング方法を非常に簡単に指示できます。型指定された DataTemplate には x:Key 値が割り当てられていませんが、Type クラスのインスタンスに DataType プロパティが含まれています。WPF が ViewModel オブジェクトをレンダリングしようとすると、使用中のリソース システムに ViewModel (または基本クラス) オブジェクトと同じ DataType を持つ型指定された DataTemplate が存在するかどうかが確認されます。そのようなものが見つかった場合、このテンプレートを使用して、タブの Content プロパティによって参照される ViewModel オブジェクトをレンダリングします。
MainWindowResource.xaml ファイルにはリソース ディクショナリ (ResourceDictionary) があります。このディクショナリはメイン ウィンドウのリソース構造に追加されます。これは、ディクショナリに含まれるリソースがウィンドウで使用されるリソースのスコープに追加されることを意味します。タブのコンテンツが ViewModel オブジェクトに設定されている場合、このディクショナリから派生した型指定された DataTemplate は、タブをレンダリングするためのビュー (ユーザー コントロール) を提供します。以下の図 10 に示すように。

<!-- 这个资源字典供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>
    <!-- 其它资源在这里忽略了... -->
</ResourceDictionary>

ViewModel オブジェクトの表示にどのビューを使用するかを決定するコードを記述する必要はありません。WPF リソース システムが面倒な作業をすべて実行してくれるため、より重要な作業に集中できます。状況をさらに複雑にするために、プログラムでビューを選択することもできますが、これはほとんどの場合不要です。

データモデルとウェアハウス

アプリケーションのウィンドウを通じて ViewModel オブジェクトが読み込まれ、表示され、閉じられるのを見てきました。インフラストラクチャが完成したので、アプリケーション本体の実装の詳細を注意深く確認できます。アプリケーションの 2 つのワークスペース、[すべての顧客を表示] と [新規顧客] に入る前に、まずデータ モデルとデータ アクセス クラスを調べてみましょう。WPF のデータ オブジェクトで動作する任意の View Model クラスを作成できるため、これらのクラスの設計は MVVM 設計パターンとはほとんど関係がありません。
Customer は、デモ プログラム内の唯一の Model クラスです。このクラスには、プライマリ名、セカンダリ名、電子メール アドレスなど、企業の顧客情報を表す少数の属性が含まれています。このクラスは、標準の IDataErrorInfo インターフェイスを実装することによって検証情報も提供します。この Customer クラスは MVVM アーキテクチャや WPF アプリケーションとも何の関係もないため、このクラスは古いビジネス ライブラリから取得できます。
データはどこかから取得され、どこかに存在する必要があります。このアプリケーションでは、CustomerRepository クラスのインスタンスを使用して、すべての顧客オブジェクトを読み込み、保存します。これは XML ファイルから顧客データをロードするときに発生しますが、外部データ ソースの種類とは関係ありません。データは、データベース、Web サービス、名前付きチャネル、ハード ドライブ上のファイル、さらには伝書バトから取得される可能性があります。実際には、それは問題ではありません。.NET オブジェクトにデータがある限り、データの取得元は関係ありません (MVVM デザイン パターンはウィンドウからデータを取得できます)。
CustomerRepository クラスは、有効な Customer オブジェクトの取得、リポジトリへの顧客の追加、リポジトリに顧客が既に含まれているかどうかの確認を可能にするメソッドを公開します。アプリケーションではユーザーによる顧客の削除が許可されていないため、ウェアハウスでも顧客の削除は許可されていません。CustomerAdded イベントは、AddCustomer メソッドを使用して顧客が CustomerRepository に追加されるときに応答されます。
明らかに、このアプリケーションのデータ モデルは、実際のビジネス アプリケーションが必要とするものに比べて非常に小さいですが、それは問題ではありません。ViewModel クラスが Customer と CustomerRepository をどのように使用するかを理解することが重要です。CustomerViewModel は Customer オブジェクトのラッパーであることに注意してください。一連のプロパティは、Customer の状態と CustomerView コントロールで使用されるその他の状態を公開するために使用されます。CustomerViewModel は Customer の状態をコピーしませんが、次のように委任を通じて公開します。

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

ユーザーが CustomerView コントロールで新しい顧客を作成し、[保存] ボタンをクリックすると、ビューに関連付けられた CustomerViewModel によって Customer オブジェクトが CustomerRepository に追加されます。これにより、CustomerRepository の CustomerAdded イベントがトリガーされ、新しい CustomerViewModel を AllCustomers コレクションに追加するように AllCustomersViewModel に通知することを目的としています。ある意味、CustomerRepository は、さまざまな Customer オブジェクトを処理する ViewModel の同期メカニズムとして機能します。これを中間デザイン パターンを使用していると見る人もいるかもしれません。次の章では、このメカニズムがどのように機能するかについて詳しく紹介します。ここで、図 11 のブロック図を参照して、これらの部分がどのように組み合わされるかを詳しく見てみましょう。
図 11 顧客との関係

新規顧客データ入力フォーム

ユーザーが [新しい顧客の作成] リンクをクリックすると、MainWindowViewModel は新しい CustomerViewModel をワークスペース リストに追加し、それを表示する CustomerView コントロールを追加します。ユーザーが入力フィールドに有効な値を入力すると、「保存」ボタンが有効になり、新しい顧客の情報を保存できるようになります。入力検証と保存ボタンを備えた通常のデータ入力フォームほど平凡なものはありません。
Customer クラスは、IDataErrorInfo インターフェイスを実装することで内部検証をサポートします。この検証により、顧客がプライマリ名、適切な形式の電子メール アドレス、および (顧客が個人の場合) セカンダリ名を持っていることが確認されます。顧客の IsCompany プロパティが true の場合、セカンダリ名に値は許可されません (会社名にはセカンダリ名がありません)。この検証ロジックは Customer オブジェクトの観点からは理にかなっていますが、これはユーザー インターフェイスに表示する必要があるものではありません。ユーザー インターフェイスに必要なのは、新しく作成された顧客が個人であるか企業であるかをユーザーが選択できるようにすることです。Customer タイプ セレクターの初期値は「未選択」です。顧客の IsCompany 属性の値が true または false のみを許可されている場合、ユーザー インターフェイスはユーザー タイプが選択されていないことをユーザーにどのように通知する必要がありますか?
ソフトウェア システム全体を完全に制御できると仮定すると、IsCompany プロパティを、選択されていない値をサポートする null 許容のブール値に変更できます。しかし、実際の状況はそれほど単純ではありません。Customer クラスは社内の別のチームの古いライブラリからのものであるため、このクラスを変更できないとします。既存のデータ構造が原因で、「未チェック」値を保存する簡単な方法がない場合はどうすればよいですか? 他のアプリケーションが既に Customer クラスを使用しており、このプロパティが通常のブール値であることに依存している場合はどうなるでしょうか? 繰り返しますが、これは ViewModel を使用することで解決できます。
図 12 のテスト メソッドは、CustomerViewModel のこの機能がどのように使用されるかを示しています。CustomerViewModel は CustomerTypeOptions プロパティを公開するので、顧客タイプ セレクターに 3 つの文字列を表示できるようになります。また、セレクターで選択された文字列を保存するために使用される CustomerType プロパティも公開します。CustomerType が設定されると、文字列値が基になる Customer オブジェクトの IsCompany プロパティのブール値にマップされます。図 13 は、これら 2 つのプロパティを示しています。

// 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");
}
// 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 コントロールには、次のようにこれら 2 つのプロパティをバインドするドロップダウン リスト (ComboBox) が含まれています。

<ComboBox ItemsSource="{Binding CustomerTypeOptions}" 
              SelectedItem="{Binding CustomerType, ValidatesOnDataErrors=True}" />

ドロップダウン リストで選択した項目が変更されると、データ ソースの IDataErrorInfo インターフェイスがクエリされて、新しい値が有効かどうかが確認されます。これは、SelectedItem プロパティ バインディングの ValidatesOnDataErrors が true に設定されているために発生します。データ ソースは CustomerViewModel オブジェクトであるため、バインディング システムは CustomerViewModel に CustomerType プロパティの検証エラー情報を要求します。ほとんどの場合、CustomerViewModel はすべての偽認証リクエストを、それに含まれる Customer オブジェクトに委任します。ただし、Customer の IsCompany プロパティには未チェック状態が定義されていないため、CustomerViewModel クラスは、ドロップダウン リストで新しく選択された項目を検証するタスクを実装する必要があります。コードは図 14 に示すとおりです。

// In CustomerViewModel.cs 
string IDataErrorInfo.this[string propertyName]
{
    
    
    get
    {
    
    
        string error = null; if (propertyName == "CustomerType")
        {
    
    
            // Customer类的IsCompany属性是一个布尔值,所以它没有“未选中”这  
            //个状态值. 
            //CustomerViewModel类处理这个映射和验证 
            error = this.ValidateCustomerType();
        }
        else {
    
     error = (_customer as IDataErrorInfo)[propertyName]; }
        //将命令注册到CommadManager,例如Save命令,实现对命名是否执行的查询.
        CommandManager.InvalidateRequerySuggested();
        return error;
    }
}
string ValidateCustomerType()
{
    
    
    if (this.CustomerType == "Company" || this.CustomerType == "Person")
        return null;
    return "Customer type must be selected";
}

このコードの重要な点は、CustomerViewModel の IDataErrorInfo 実装を使用して、ViewModel 固有のプロパティの検証リクエストを処理し、Customer オブジェクトからの他のリクエストを委任することです。これにより、Model クラスで検証ロジックを使用し、ViewModel クラスでのみ意味をなす追加の検証プロパティを持つことができます。
ビューは、SaveCommand プロパティを通じて CustoemrViewModel を保存できます。SaveCommand は、RelayCommand クラスがチェックする前に、CustomerViewModel が自身を保存できるかどうか、およびその状態を保存するように指示されたときに何をするかを決定できるようにするために使用されます。このアプリケーションでは、新しい顧客を保存することは、顧客を CustomerRepository に追加するだけです。新規顧客を救えるかどうかは 2 つの側面によって決まります。新しい顧客が Customer オブジェクトで有効かどうかを尋ね、CustomerViewModel で有効かどうかを判断します。最初に ViewModel 固有のプロパティのチェックと検証が行われるため、二重検証が必要です。CustomerViewModel の保存ロジックは図 15 に示すとおりです。

// 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 オブジェクトや「未チェック」状態のブール プロパティなどのイベントを表示するビューを簡単に作成できます。また、クライアントに状態を保存するように通知する機能も提供します。ビューが Customer オブジェクトに直接バインドされている場合、ビューにはこれらの関数を実装するために多くのコードが必要になります。標準の MVVM アーキテクチャでは、ビューのバックグラウンド コードのほとんどは空であるか、多くてもビュー内のリソースと操作制御コードのみが含まれている必要があります。場合によっては、イベントのトリガーやメソッドの呼び出しなど、View で ViewModel オブジェクトと対話するための舞台裏のコードを記述することも必要になりますが、通常、これを ViewModel 自体で呼び出すのは困難です。

すべての顧客ビュー

デモ アプリには、すべての顧客のリストを表示するワークスペースが含まれています。リスト内の顧客は、入力された個人または法人に従ってグループ化されます。ユーザーは 1 人以上の顧客を同時に選択し、右下隅にこれらの顧客の合計売上高を表示できます。
ユーザー インターフェイスは、AllCustomerViewModel オブジェクトをレンダリングする AllCustomerView コントロールです。各リスト項目 (ListViewItem) は、AllCustomerViewModel オブジェクトによって公開される AllCustomers コレクション内の CustomerViewModel オブジェクトを表します。前の章では、CustomerViewModel がデータ入力フォームとしてどのようにレンダリングされるかを説明しましたが、今度は、同じ CustomerViewModel オブジェクトがリスト内の項目としてレンダリングされます。CustomerViewModel クラスは、表示されるビジュアル要素のタイプについて何も知らないため、何度も使用できます。
AllCustomerView は、リストの ItemsSource を CollectionViewSource にバインドすることにより、リストに表示されるグループを作成します。CollectionViewSource の構成は図 16 に示すとおりです。

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

リスト項目と CustomerViewModel オブジェクト間の関連付けは、リストの ItemsContainerStyle プロパティによって確立されます。リスト項目のプロパティを CustomerViewModel プロパティにバインドできるようにするスタイル (Style) が、各リスト項目のプロパティに割り当てられます。重要なバインディング スタイルは、次のように、リスト項目の 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 が選択されているかどうかに応じて、選択された顧客の合計数が変わります。AllCustomerViewModel クラスは、リストの下にある ContentPresenter が正しい値を表示できるように、この値を維持する役割を果たします。図 17 は、AllCustomerViewModel が各顧客が選択されているかどうかを監視し、表示された値を更新するようにビューに通知する方法を示しています。

// 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";
    //确保我们引用的属性是有效的
    //这个一个调试技术,在Release生成中不会执行
    (sender as CustomerViewModel).VerifyPropertyName(IsSelected);
    //当一个客户的选择状态发生变更时,需要让系统知道选中的总数量的属性发生乐变化,
    //以便为了该属性获取一个新的值 
    if (e.PropertyName == IsSelected)
        this.OnPropertyChanged("TotalSelectedSales");
}

UI は TotalSelectedSales プロパティをバインドし、通貨書式設定を適用して値を表示します。double 値の代わりに TotalSelectedSales プロパティから文字列を返すことにより、ViewModel オブジェクト (View ではない) に通貨書式設定を適用します。ContentStringFormat 属性は .NET Framework 3.5 SP1 の ContentPresenter に追加されるため、古いバージョンの WPF アプリケーションの場合は、以下に示す通貨形式コードを追加する必要があります。

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

要約する

WPF はアプリケーション開発者に多くの機能を提供しますが、開発者は、学んだことを適用し、WPF の機能をさらに活用するには考え方を変える必要があります。MVVM デザイン パターンは、WPF アプリケーションを設計および実装するためのシンプルで効果的なガイドです。これにより、データ、ロジック、表示の独立性が高く、混乱が起こりにくい開発ソフトウェアを作成できます。

おすすめ

転載: blog.csdn.net/weixin_37537723/article/details/106916294