ビューモデルを使用する

完了

MVVM パターンを構成するコンポーネントについて学んだので、モデルとビューを簡単に定義できることがおわかりになったことでしょう。 ビューモデルを使用して、パターンでそのロールをより適切に定義する方法を探りましょう。

ユーザー インターフェイスにプロパティを公開する

前の例のように、通常、ビューモデルでは、そのほとんどのデータとあらゆるビジネス ロジックをモデルに依存します。 ただし、現在のビューで必要な方法でデータの書式設定、変換、強化が行われるのはビューモデルです。

ビューモデルを使用して書式設定する

書式設定の例は休暇時間で既に見ました。 日付の書式設定、文字エンコード、シリアル化は、すべて、ビューモデルでモデルからデータの書式設定が行われる場合の例です。

ビューモデルを使用して変換する

多くの場合、モデルでは間接的な方法で情報が提供されます。 しかし、ビューモデルでそれを修正できます。 たとえば、従業員が監督者であるかどうかを画面に表示するとします。 しかし、Employee モデルではそれは直接示されません。 代わりに、その人に、自分への報告を行う他の人が存在するかどうかに基づいて、この事実を推測する必要があります。 モデルに次のプロパティがあるとします。

public IList<Employee> DirectReports
{
    get
    {
        ...
    }
}

このリストが空の場合は、この Employee は監督者ではないと推測できます。 この場合、EmployeeViewModel には、そのロジックを提供するプロパティ IsSupervisor が含まれます。

public bool IsSupervisor => _model.DirectReports.Any();

ビューモデルを使用して強化する

モデルで関連するデータの ID のみが提供される場合があります。 または、1 つの画面に必要なデータを関連付けるために、複数のモデル クラスを参照することが必要な場合があります。 ビューモデルは、これらのタスクを実行する理想的な場所でもあります。 ある従業員が現在管理しているすべてのプロジェクトを表示したいとします。 このデータは、Employee モデル クラスの一部ではありません。 CompanyProjects モデル クラスを参照することによってアクセスできます。 EmployeeViewModel では、いつものように、その作業がパブリック プロパティとして公開されます。

public IEnumerable<string> ActiveProjects => CompanyProjects.All
    .Where(p => p.Owner == _model.Id && p.IsActive)
    .Select(p => p.Name);

ビューモデルでパススルー プロパティを使用する

ビューモデルでは、モデルで提供されるプロパティ "そのもの" が必要になることがよくあります。 それらのプロパティに対して、ViewModel では単にデータをパススルーします。

public string Name
{
    get => _model.Name;
    set => _model.Name = value;
}

ビューモデルのスコープを設定する

ビューモデルは、ビューが存在する任意のレベルで使用できます。 通常、ビューモデルはページで "所有" されますが、そのページのサブビューでも所有できます。 ビューモデルを入れ子にする一般的な理由の 1 つは、ページ上に ListView が表示される場合です。 このリストには、EmployeeListViewModel など、コレクションを表す ViewModel があります。 リスト内の各要素は EmployeeViewModel です。

複数の EmployeeViewModel サブオブジェクトを持つ EmployeeListViewModel の図。

アプリケーション全体のデータと状態を保持しても、特定のページに関連付けられていない、最上位レベルのビューモデルを使うことも一般的です。 そのようなビューモデルは、"アクティブ" な項目を維持するためによく使われます。 先ほど説明した ListView の例について考えてみます。 ユーザーが従業員の行を選択すると、"現在の項目" はその従業員となります。 その行を選択したままで、ユーザーが詳細ページに移動したり、ツール バーのボタンを選択したりした場合、そのアクションや表示はその従業員を対象としたものになる必要があります。 このシナリオを処理するための洗練された方法は、ツール バーや詳細ページでもアクセスできるプロパティに ListView.SelectItem をデータ バインドすることです。 中央のビューモデルにそのプロパティを置くとうまくいきます。

ViewModel と View を再利用するタイミングを特定する

ビューモデルとモデル間のリレーションシップ、およびビューモデルとビュー間のリレーションシップを定義する方法は、規則よりアプリの要件によって示されます。 ビューモデルの目的は、必要な構造とデータをビューに提供することです。 それによって、ビューモデルをスコープとするための "大きさ" に関する決定をガイドする必要があります。

ビューモデルには、モデル クラスの構造が密接に反映されることがよくあり、そのクラスとの間に一対一のリレーションシップがあります。 前に EmployeeViewModel を使う例について説明しましたが、これでは 1 つの Employee インスタンスをラップして拡張していました。 ただし、常に一対一のリレーションシップになるわけではありません。 ビューモデルがビューに必要な内容を提供するように設計されている場合、どのモデルとも明示的なリレーションシップを持たないものの、"任意の" モデル クラスのデータを使える、HR 部署の概要を示す HRDashboardViewModel のようなものを、代わりに作成できます。

同様に、ビューモデルと "ビュー" が一対一のリレーションシップを持つ場合がよくあることに気づくかもしれません。 しかし、これも必ずそうなるわけではありません。 もう一度、各従業員の行を表示する ListView について考えてみましょう。 いずれかの行を選択すると、従業員の詳細ページに移動します。

リスト ページには、そのビューモデルとコレクションがあります。 以前に示唆したように、そのコレクションは EmployeeViewModel オブジェクトのコレクションにすることが "できます"。 さらに、ユーザーが行を選択したときに、EmployeeViewModel インスタンスを EmployeeDetailPage に渡すことが "できます"。 また、詳細ページでは、その EmployeeViewModel をその BindingContext として使うことが "できます"。

このシナリオは、ビューモデルを再利用する絶好の機会 "かもしれません"。 ただし、ビューモデルの目的は、ビューに必要なものを提供することであることを忘れないでください。 場合によっては、すべて同じモデル クラスに基づいていても、別のビューモデルを使いたいことがあります。 この例では、ListView の行に必要な情報は、詳細ページ全体よりもずっと少なくなりそうです。 詳細ページに必要なデータを取得すると多くのオーバーヘッドが追加される場合は、これらのそれぞれのビューに情報を提供する EmployeeListRowViewModelEmployeeDetailViewModel の両方のモデルを使うことができます。

ビューモデル オブジェクト モデル

INotifyPropertyChanged を実装する基底クラスを使用すると、すべてのビューモデルにインターフェイスを再実装する必要はありません。 このトレーニング モジュールの前のパートで説明したように、HR アプリケーションについて考えてみます。 EmployeeViewModel クラスでは INotifyPropertyChanged インターフェイスを実装し、PropertyChanged イベントを発生する OnPropertyChanged という名前のヘルパー メソッドを提供しました。 プロジェクト内の他のビューモデル (たとえば、従業員に割り当てられたリソースを記述するもの) でも、ビューと完全に統合するために INotifyPropertyChanged が必要です。

.NET Community Toolkit に含まれている MVVM Toolkit ライブラリは、MVVM パターンを使用して最新のアプリを構築するための最初の実装を提供する、標準、自己完結、軽量の型のコレクションです。

独自のビューモデルの基底クラスを記述する代わりに、ツールキットの ObservableObject クラス (ビューモデルの基底クラスに必要なものがすべて用意されています) から継承します。 EmployeeViewModel は、次から簡素化できます

using System.ComponentModel;

public class EmployeeViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    private Employee _model;

    public string Name
    {
        get {...}
        set
        {
            _model.Name = value;
            OnPropertyChanged(nameof(Name))
        }
    }

    protected void OnPropertyChanged(string propertyName) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

次のコードにします。

using Microsoft.Toolkit.Mvvm.ComponentModel;

public class EmployeeViewModel : ObservableObject
{
    private string _name;

    public string Name
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }
}

MVVM Toolkit は、CommunityToolkit.Mvvm NuGet パッケージを通じて配布されます。

自分の知識をチェックする

1.

.NET MAUI と共に MVVM パターンを使う場合、モデル、ビュー、ビューモデルが互いに完全に分離することはありません。 MVVM の各部分間の一般的な依存関係の 1 つを表しているのはどの選択肢ですか?

2.

モデル、ビュー、ビューモデルのうち、プラットフォームと密接に連動し、単体テストを作成するのが困難である可能性が最も高いのはどれですか?