Model-View-ViewModel (MVVM)
ヒント
この内容は電子ブック『.NET MAUI を使用したエンタープライズ アプリケーション パターン』からの抜粋です。これは .NET Docs で閲覧することも、無料の PDF をダウンロードしてオフラインで読むこともできます。
.NET MAUI 開発者エクスペリエンスでは、通常、XAML でユーザー インターフェイスを作成してから、そのユーザー インターフェイスで動作する分離コードを追加します。 アプリが変更され、サイズとスコープが拡大すると、複雑なメンテナンスの問題が発生する可能性があります。 これらの問題には、UI コントロールとビジネス ロジックとの密結合が含まれます。これにより、UI の変更を行うコストが増加し、このようなコードを単体テストすることが困難になります。
MVVM パターンは、アプリケーションのビジネスおよびプレゼンテーション ロジックをそのユーザー インターフェイス (UI) からクリーンに分離するのに役立ちます。 アプリケーション ロジックと UI の間でクリーンな分離を維持することは、多くの開発の問題に対処するのに役立ち、アプリケーションのテスト、維持、および進化が容易になります。 また、コード再利用の機会を大幅に向上させ、開発者や UI デザイナーがアプリのそれぞれの部分を開発するときに共同作業をより容易にすることができます。
MVVM パターン
MVVM パターンには、モデル、ビュー、ビュー モデルの 3 つの主要なコンポーネントがあります。 それぞれが異なる目的を果たします。 次の図は、3 つのコンポーネント間の関係を示しています。
各コンポーネントの役割を理解するだけでなく、それらがどのようにやりとりするかを理解することも重要です。 大まかに言うと、ビューはビュー モデルを "認識" し、ビュー モデルはモデルを "認識" しますが、モデルはビュー モデルを認識しておらず、ビュー モデルはビューを認識していません。 したがって、ビュー モデルではビューをモデルから分離させ、ビューとは独立してモデルを進化させることができます。
MVVM パターンを使用する利点は次のとおりです。
- 既存のモデル実装で既存のビジネス ロジックがカプセル化されていると、変更が困難または危険な場合があります。 このシナリオでは、ビュー モデルはモデル クラスのアダプターとして機能し、モデル コードに大きな変更が加えられるのを防ぎます。
- 開発者は、ビューを使用せずに、ビュー モデルとモデルの単体テストを作成できます。 ビュー モデルの単体テストでは、ビューで使用されるのとまったく同じ機能を実行できます。
- ビューが XAML または C# で完全に実装されている場合は、ビュー モデルおよびモデル コードに触れることなくアプリ UI を再設計できます。 したがって、新しいバージョンのビューは、既存のビュー モデルで動作する必要があります。
- デザイナーと開発者は、開発時にコンポーネントで独立して同時に作業できます。 デザイナーはビューに集中でき、開発者はビュー モデルとモデルのコンポーネントに取り組むことができます。
MVVM を効果的に使用する鍵は、アプリ コードを正しいクラスに組み込む方法と、クラスがどのようにやりとりするかを理解することにあります。 次のセクションでは、MVVM パターンの各クラスの役割について説明します。
表示
ビューには、ユーザーが画面に表示するものの構造、レイアウト、外観を定義する役割があります。 各ビューが XAML で定義され、ビジネス ロジックを含まない分離コードが限られていることが理想的です。 しかし、場合によっては、分離コードには、アニメーションなどの XAML で表現するのが困難な視覚的なビヘイビアーを実装する UI ロジックが含まれていることがあります。
.NET MAUI アプリケーションでは、ビューは通常、ContentPage
派生または ContentView
派生クラスです。 しかし、ビューは、表示時にオブジェクトを視覚的に表すために使用される UI 要素を指定するデータ テンプレートで表すこともできます。 ビューとしてのデータ テンプレートには分離コードがないため、特定のビュー モデル型にバインドするように設計されています。
ヒント
分離コードで UI 要素を有効または無効にすることは避けてください。
ビュー モデルが、(コマンドが使用可能かどうかや操作が保留中であることを示すなど) ビューの表示の一部の側面に影響を与える論理状態の変更を定義する役割があることを確めてください。 したがって、分離コードで有効または無効にするのではなく、モデルのプロパティを表示するためにバインドすることで、UI 要素を有効または無効にします。
ボタンのクリックや項目の選択など、ビューでの操作に応じてビュー モデルでコードを実行するためのいくつかのオプションがあります。 コントロールでコマンドがサポートされている場合、そのコントロールの Command プロパティは、ビュー モデルの ICommand プロパティにデータバインドできます。 コントロールのコマンドが呼び出されると、ビュー モデル内のコードが実行されます。 コマンドに加えて、ビヘイビアーはビュー内のオブジェクトにアタッチでき、呼び出されるコマンドまたは発生するイベントをリッスンできます。 それに応じて、ビヘイビアーではその後、ビュー モデルまたはそのビュー モデルのメソッドで ICommand を呼び出すことができます。
ViewModel
ビュー モデルでは、ビューでデータ バインドできるプロパティとコマンドを実装し、変更通知イベントを通じて状態の変更をビューに通知します。 ビュー モデルで提供されるプロパティとコマンドでは、UI によって提供される機能を定義しますが、その機能の表示方法はビューで決定されます。
ヒント
非同期操作で UI の応答性を維持します。
マルチプラットフォーム アプリでは、ユーザーのパフォーマンス認識を向上させるために、UI スレッドのブロックを解除したままにする必要があります。 したがって、ビュー モデルでは、I/O 操作に非同期メソッドを使用し、イベントを発生させ、プロパティの変更をビューに非同期的に通知します。
ビュー モデルには、必要なモデル クラスとビューのやりとりを調整する役割もあります。 通常、ビュー モデルとモデル クラスの間には一対多の関係があります。 ビュー モデルでは、ビュー内のコントロールが直接データ バインドできるように、モデル クラスをビューに直接公開することを選択する場合があります。 この場合、モデル クラスは、データ バインディングと変更通知イベントをサポートするように設計される必要があります。
各ビュー モデルでは、ビューで簡単に使用できる形式のモデルからのデータが提供されます。 これを実現するために、ビュー モデルではデータ変換を行うことがあります。 ビュー モデルにこのデータ変換を配置することをお勧めします。これにより、ビューがバインドできるプロパティが提供されるためです。 たとえば、ビュー モデルでは、2 つのプロパティの値を組み合わせて、ビューで表示しやすくすることができます。
ヒント
変換レイヤーでデータ変換を一元化します。
また、ビュー モデルとビューの間に位置する個別のデータ変換レイヤーとしてコンバーターを使用することもできます。 これは、たとえば、ビュー モデルで提供されない特殊な書式がデータに必要な場合などに必要になることがあります。
ビュー モデルがビューとの双方向データ バインディングに参加するには、そのプロパティで PropertyChanged
イベントを発生させる必要があります。 ビュー モデルは、INotifyPropertyChanged
インターフェイスを実装し、プロパティが変更されたときに PropertyChanged
イベントを発生させることで、この要件を満たします。
コレクションの場合は、ビュー フレンドリな ObservableCollection<T>
が提供されます。 このコレクションでは、コレクションの変更通知を実装するため、開発者はコレクションに INotifyCollectionChanged
インターフェイスを実装する必要がなくなります。
モデル
モデル クラスは、アプリのデータをカプセル化する非ビジュアル クラスです。 したがって、このモデルはアプリのドメイン モデルを表すものと考えることができます。通常、これにはビジネスおよび検証ロジックと共にデータ モデルが含まれます。 モデル オブジェクトの例として、データ転送オブジェクト (DTO)、単純な従来の CLR オブジェクト (POCO)、生成されたエンティティおよびプロキシ オブジェクトなどがあります。
通常、モデル クラスは、データ アクセスとキャッシュをカプセル化するサービスまたはリポジトリと組み合わせて使用されます。
ビューへのビュー モデルの接続
ビュー モデルは、.NET MAUI のデータバインディング機能を使用してビューに接続できます。 ビューを構築し、モデルを表示し、実行時にそれらを関連付けるために使用できる多くのアプローチがあります。 これらのアプローチは、ビューの最初のコンポジション、およびビュー モデルの最初のコンポジションと呼ばれる 2 つのカテゴリに分類されます。 ビューの最初のコンポジションとビュー モデルの最初のコンポジションの選択では、優先順位と複雑さが問題となります。 しかし、すべてのアプローチで同じ目的が共有されます。これは、ビューで BindingContext プロパティにビュー モデルを割り当てることです。
ビューの最初のコンポジションでは、アプリは概念的に、それらが依存するビュー モデルに接続するビューで構成されます。 このアプローチの主な利点は、ビュー モデルがビュー自体に依存しないため、疎結合の単体テスト可能なアプリを簡単に構築できることです。 また、クラスがどのように作成されて関連付けられるかを理解するためにコードの実行を追跡する必要はなく、その視覚的な構造に従ってアプリの構造を理解することも簡単です。 さらに、ビューの最初のコンポジションは、ナビゲーションが発生したときにページを構築する役割がある Microsoft Maui のナビゲーション システムに合わせて調整されます。これにより、ビュー モデルの最初のコンポジションは複雑になり、プラットフォームと不整合になります。
ビュー モデルの最初のコンポジションでは、アプリは概念的にビュー モデルで構成され、ビュー モデルのビューを検索する役割があるサービスが使用されます。 ビュー モデルの最初のコンポジションは、ビューの作成を抽象化できるため、一部の開発者にはより自然に感じられ、アプリの論理的な非 UI 構造に集中できます。 さらに、ビュー モデルを他のビュー モデルで作成することもできます。 しかし、多くの場合、このアプローチは複雑であり、アプリのさまざまな部分がどのように作成され、関連付けられているかを理解するのが難しくなる可能性があります。
ヒント
ビュー モデルとビューを独立した状態に保ちます。
データ ソース内のプロパティへのビューのバインドは、ビューの対応するビュー モデルに対する主な依存関係である必要があります。 具体的には、ビュー モデルから Button や ListView などのビューの種類を参照しません。 ここで概説する原則に従うことで、ビュー モデルを分離してテストできるため、スコープを制限することでソフトウェアの欠陥の可能性を減らすことができます。
次のセクションでは、ビュー モデルをビューに接続する主なアプローチについて説明します。
ビュー モデルの宣言的な作成
最も簡単なアプローチは、ビューで、XAML で対応するビュー モデルを宣言的にインスタンス化することです。 ビューが構築されると、対応するビュー モデル オブジェクトも構築されます。 このアプローチは次のコード例で示されています。
<ContentPage xmlns:local="clr-namespace:eShop">
<ContentPage.BindingContext>
<local:LoginViewModel />
</ContentPage.BindingContext>
<!-- Omitted for brevity... -->
</ContentPage>
ContentPage
が作成されると、LoginViewModel
のインスタンスが自動的に構築され、ビューの BindingContext
として設定されます。
ビューによるこの宣言型の構築とビュー モデルの割り当ては、簡単であるという利点がありますが、ビュー モデルに既定の (パラメーターレス) コンストラクターが必要であるという欠点があります。
プログラムによるビュー モデルの作成
ビューでは分離コード ファイルにコードを含めることができます。その結果、ビュー モデルがその BindingContext
プロパティに割り当てられます。 これは、多くの場合、次のコード例に示すように、ビューのコンストラクターで実現されます。
public LoginView()
{
InitializeComponent();
BindingContext = new LoginViewModel(navigationService);
}
ビューの分離コード内でのビュー モデルのプログラムによる構築と割り当てには、簡単であるという利点があります。 しかし、このアプローチの主な欠点は、ビューで必要な依存関係をビュー モデルに提供する必要があることです。 依存関係挿入コンテナーを使用すると、ビューとビュー モデルの間の疎結合を維持するのに役立つ場合があります。 詳細については、依存性の注入に関するページを参照してください。
基になるビュー モデルまたはモデルの変更に応じてビューを更新する
ビューにアクセスできるすべてのビュー モデルおよびモデル クラスでは、INotifyPropertyChanged インターフェイスを実装する必要があります。 ビュー モデルまたはモデル クラスにこのインターフェイスを実装すると、基になるプロパティ値が変更されたときに、ビュー内のデータバインド コントロールに変更通知を提供できます。
アプリは、次の要件を満たすことで、プロパティ変更通知を正しく使用できるように設計する必要があります。
- パブリック プロパティの値が変更された場合でも、常に
PropertyChanged
イベントを発生させる。 XAML バインディングがどのように発生するかがわかっているのでPropertyChanged
イベントを発生させることを無視できると思い込まないでください。 - ビュー モデルまたはモデルの他のプロパティで値が使用される計算プロパティの
PropertyChanged
イベントを常に発生させる。 - プロパティを変更するメソッドの最後に、またはオブジェクトが安全な状態であることが判明した場合は、常に
PropertyChanged
イベントを発生させる。 イベントを発生させると、そのイベントのハンドラーを同期的に呼び出すことによって操作が中断されます。 これが操作の途中で発生した場合、オブジェクトが安全ではなく、部分的に更新された状態のときにコールバック関数に公開される可能性があります。 さらに、カスケード変更がPropertyChanged
イベントによってトリガーされる可能性があります。 カスケード変更では、通常、カスケード変更を安全に実行する前に更新を完了する必要があります。 - プロパティが変更されない場合でも、
PropertyChanged
イベントを発生させない。 つまり、PropertyChanged
イベントを発生させる前に、古い値と新しい値を比較する必要があります。 - プロパティを初期化する場合は、ビュー モデルのコンストラクター中に
PropertyChanged
イベントを発生させない。 ビュー内のデータバインド コントロールは、この時点で変更通知を受信するためにサブスクライブされていません。 - クラスのパブリック メソッドの単一の同期呼び出し内で、同じプロパティ名引数を持つ複数の
PropertyChanged
イベントを発生させない。 たとえば、バッキング ストアが_numberOfItems
フィールドであるNumberOfItems
プロパティの場合、ループの実行中にメソッドで 50 回_numberOfItems
がインクリメントされた場合、すべての作業が完了した後、NumberOfItems
プロパティに対してプロパティ変更通知を 1 回だけ発生させる必要があります。 非同期メソッドの場合は、非同期継続チェーンの各同期セグメントで、特定のプロパティ名に対してPropertyChanged
イベントを発生させます。
この機能を提供する簡単な方法は、BindableObject
クラスの拡張を作成することです。 この例では、次のコード例に示すように、ExtendedBindableObject
クラスによって変更通知が提供されます。
public abstract class ExtendedBindableObject : BindableObject
{
public void RaisePropertyChanged<T>(Expression<Func<T>> property)
{
var name = GetMemberInfo(property).Name;
OnPropertyChanged(name);
}
private MemberInfo GetMemberInfo(Expression expression)
{
// Omitted for brevity ...
}
}
.NET MAUI の BindableObject
クラスでは INotifyPropertyChanged
インターフェイスを実装し、OnPropertyChanged
メソッドを提供します。 この ExtendedBindableObject
クラスでは、プロパティ変更通知を呼び出す RaisePropertyChanged
メソッドを提供します。その場合、BindableObject
クラスによって提供される機能が使用されます。
その後、ビュー モデル クラスをその ExtendedBindableObject
クラスから派生できます。 したがって、各ビュー モデル クラスでは、ExtendedBindableObject
クラス内の RaisePropertyChanged
メソッドを使用してプロパティ変更通知を提供します。 次のコード例は、eShop マルチプラットフォーム アプリがラムダ式を使用してどのようにプロパティ変更通知を呼び出すかを示しています。
public bool IsLogin
{
get => _isLogin;
set
{
_isLogin = value;
RaisePropertyChanged(() => IsLogin);
}
}
この方法でラムダ式を使用すると、呼び出しごとにラムダ式を評価する必要があるため、パフォーマンス コストが小さくなります。 パフォーマンス コストは小さく、通常はアプリに影響しませんが、多くの変更通知がある場合にコストが発生する可能性があります。 しかし、このアプローチの利点は、プロパティの名前を変更するときにコンパイル時の型の安全性とリファクタリングのサポートが提供されることです。
MVVM フレームワーク
MVVM パターンは .NET で十分に確立されており、コミュニティではこの開発を容易にするのに役立つ多くのフレームワークが作成されています。 各フレームワークでは異なる機能セットが提供されますが、INotifyPropertyChanged
インターフェイスを実装して一般的なビュー モデルを提供するのが標準的です。 MVVM フレームワークの追加機能には、カスタム コマンド、ナビゲーション ヘルパー、依存関係の挿入/サービス ロケーター コンポーネント、UI プラットフォームの統合などがあります。 これらのフレームワークを使用する必要はありませんが、開発を高速化して標準化できます。 eShop マルチプラットフォーム アプリは .NET Community MVVM Toolkit を使用します。 フレームワークを選ぶときは、アプリケーションのニーズとチームの長所を考慮する必要があります。 以下のリストには、.NET MAUI 用のより一般的な MVVM フレームワークの一部が含まれています。
コマンドとビヘイビアーを使用した UI 操作
マルチプラットフォーム アプリでは、通常、ボタン クリックなどのユーザー アクションに応じてアクションが呼び出されます。これは、分離コード ファイルにイベント ハンドラーを作成することによって実装できます。 しかし、MVVM パターンでは、アクションを実装する役割はビュー モデルにあり、分離コードにコードを配置することは避ける必要があります。
コマンドでは、UI のコントロールにバインドできるアクションを表す便利な方法が提供されます。 アクションを実装するコードをカプセル化し、ビュー内の視覚的表現から切り離しておくのに役立ちます。 このようにすると、ビュー モデルはプラットフォームの UI フレームワークによって提供されるイベントに直接依存しないため、新しいプラットフォームへの移植性がより高くなります。 .NET MAUI には、コマンドに宣言的に接続できるコントロールが含まれており、これらのコントロールでは、ユーザーがコントロールを操作するときにコマンドを呼び出します。
ビヘイビアーでは、コントロールをコマンドに宣言的に接続することもできます。 しかし、ビヘイビアーを使用して、コントロールによって発生するイベントの範囲に関連付けられているアクションを呼び出すことができます。 したがって、ビヘイビアーでは、より高い柔軟性とコントロールを提供しながら、コマンドが有効なコントロールと同じシナリオの多くに対処します。 さらに、ビヘイビアーを使用して、コマンド オブジェクトまたはメソッドを、コマンドとやりとりするように特別に設計されていないコントロールに関連付けることもできます。
コマンドの実装
ビュー モデルでは、通常、ICommand
インターフェイスを実装する、ビューからのバインド用のパブリック プロパティを公開します。 多くの .NET MAUI コントロールとジェスチャでは、ビュー モデルによって提供される ICommand
オブジェクトにデータ バインドできる Command
プロパティが提供されます。 ボタン コントロールは、最もよく使用されるコントロールの 1 つであり、ボタンがクリックされたときに実行されるコマンド プロパティを提供します。
Note
ビュー モデルで使用される ICommand
インターフェイスの実際の実装 (例: Command<T>
や RelayCommand
) を公開することは可能ですが、コマンドを ICommand
として公開することをお勧めします。 これにより、実装を後日変更する必要がある場合に、簡単にスワップアウトできます。
ICommand
インターフェイスでは、操作自体をカプセル化する Execute
メソッド、コマンドを呼び出すことができるかどうかを示す CanExecute
メソッド、およびコマンドを実行する必要があるかどうかに影響する変更が発生したときに発生する CanExecuteChanged
イベントを定義します。 ほとんどの場合、コマンドには Execute
メソッドのみを指定します。 ICommand
の詳細については、.NET MAUI の「コマンド実行」ドキュメントを参照してください。
.NET MAUI と共に提供される Command
および Command<T>
クラスでは、ICommand
インターフェイスが実装されます。この T
は Execute
と CanExecute
に対する引数の型です。 Command
と Command<T>
は、ICommand
インターフェイスに必要な最小限の機能セットを提供する基本的な実装です。
Note
多くの MVVM フレームワークでは、ICommand
インターフェイスのより機能豊富な実装が提供されています。
Command
または Command<T>
コンストラクターには、ICommand.Execute
メソッドの呼び出し時に呼び出される Action コールバック オブジェクトが必要です。 CanExecute
メソッドは省略可能なコンストラクター パラメーターであり、ブール値を返す Func です。
eShop マルチプラットフォーム アプリは RelayCommand と AsyncRelayCommand を使用します。 最新のアプリケーションの主な利点は、AsyncRelayCommand
で非同期操作の優れた機能が提供されることです。
次のコードは、register コマンドを表す Command
インスタンスを、Register ビュー モデル メソッドへのデリゲートを指定して構築する方法を示しています。
public ICommand RegisterCommand { get; }
このコマンドは、ICommand
への参照を返すプロパティを介してビューに公開されます。 Command
オブジェクトで Execute
メソッドが呼び出されると、Command
コンストラクターで指定されたデリゲートを通じて、ビュー モデル内のメソッドへの呼び出しが単に転送されます。 非同期メソッドは、コマンドの Execute
デリゲートを指定するときに async と await キーワードを使用して、コマンドで呼び出すことができます。 これは、コールバックが Task
であり、待機する必要があることを示します。 たとえば、次のコードは、サインイン コマンドを表す ICommand
インスタンスを、SignInAsync
ビュー モデル メソッドへのデリゲートを指定して構築する方法を示しています。
public ICommand SignInCommand { get; }
...
SignInCommand = new AsyncRelayCommand(async () => await SignInAsync());
パラメーターは、AsyncRelayCommand<T>
クラスを使用してコマンドをインスタンス化することで、Execute
と CanExecute
アクションに渡すことができます。 たとえば、次のコードは、NavigateAsync
メソッドで型文字列の引数を必要とすることを示すために AsyncRelayCommand<T>
インスタンスがどのように使用されるかを示しています。
public ICommand NavigateCommand { get; }
...
NavigateCommand = new AsyncRelayCommand<string>(NavigateAsync);
RelayCommand
と RelayCommand<T>
のいずれのクラスでも、各コンストラクターの CanExecute
メソッドへのデリゲートは省略可能です。 デリゲートが指定されていない場合、Command
では CanExecute
に対して true が返されます。 しかし、ビュー モデルでは、Command
オブジェクトで ChangeCanExecute
メソッドを呼び出すことによって、コマンドの CanExecute
状態の変更を示すことができます。 これにより、CanExecuteChanged
イベントが発生します。 その後、コマンドにバインドされている UI コントロールでは、有効な状態を更新し、データバインド コマンドの可用性を反映します。
ビューからのコマンドの呼び出し
次のコード例は、TapGestureRecognizer
インスタンスを使用して、LoginView
の Grid
が LoginViewModel
クラス内の RegisterCommand
にどのようにバインドされるかを示しています。
<Grid Grid.Column="1" HorizontalOptions="Center">
<Label Text="REGISTER" TextColor="Gray"/>
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding RegisterCommand}" NumberOfTapsRequired="1" />
</Grid.GestureRecognizers>
</Grid>
CommandParameter
プロパティを使用して、必要に応じてコマンド パラメーターを定義することもできます。 予期される引数の型は、Execute
および CanExecute
ターゲット メソッドで指定されます。 TapGestureRecognizer
では、ユーザーがアタッチされたコントロールを操作すると、ターゲット コマンドが自動的に呼び出されます。 CommandParameter
(指定されている場合) は、コマンドの Execute デリゲートに引数として渡されます。
ビヘイビアーの実装
ビヘイビアーを使用すると、サブクラス化することなく、機能を UI コントロールに追加できます。 代わりに、その機能はビヘイビアー クラスで実装され、それがコントロール自体の一部であるかのようにコントロールにアタッチされます。 ビヘイビアーを使うと、通常は分離コードとして記述する必要があるコードを実装できます。これは、コントロールに簡潔にアタッチされ、複数のビューまたはアプリ全体で再利用できるようにパッケージ化される方法で、コントロールの API と直接やりとりしているためです。 MVVM のコンテキストでは、ビヘイビアーは、コントロールをコマンドに接続するための便利なアプローチです。
アタッチされたプロパティを介してコントロールにアタッチされるビヘイビアーは、''アタッチされたビヘイビアー'' と呼ばれます。 その後、ビヘイビアーでは、アタッチ先の要素の公開された API を使用して、ビューのビジュアル ツリーでそのコントロールまたはその他のコントロールに機能を追加できます。
.NET MAUI ビヘイビアーは、Behavior
または Behavior<T>
クラスから派生するクラスです。この T は、ビヘイビアーを適用する必要があるコントロールの種類です。 これらのクラスでは、OnAttachedTo
および OnDetachingFrom
メソッドが提供されます。これらは、ビヘイビアーがコンロールに対してアタッチおよびデタッチされるときに実行されるロジックを提供するためにオーバーライドする必要があります。
eShop マルチプラットフォーム アプリでは、BindableBehavior<T>
クラスは Behavior<T>
クラスから派生します。 BindableBehavior<T>
クラスの目的は、ビヘイビアーの BindingContext
をアタッチされたコントロールに設定する必要がある .NET MAUI ビヘイビアーの基本クラスを提供することです。
BindableBehavior<T>
クラスでは、ビヘイビアーの BindingContext
を設定するオーバーライド可能な OnAttachedTo
メソッドと、BindingContext
をクリーンアップするオーバーライド可能な OnDetachingFrom
メソッドが提供されます。
eShop マルチプラットフォーム アプリには、MAUI Community ツールキットによって提供される EventToCommandBehavior クラスが含まれています。 EventToCommandBehavior
では、発生しているイベントに応じてコマンドを実行します。 このクラスは BaseBehavior<View>
クラスから派生し、ビヘイビアーが使用されるときに Command
プロパティで指定された ICommand
にそのビヘイビアーをバインドして実行できるようにします。 次に示すのは、EventToCommandBehavior
クラスのコード例です。
/// <summary>
/// The <see cref="EventToCommandBehavior"/> is a behavior that allows the user to invoke a <see cref="ICommand"/> through an event. It is designed to associate Commands to events exposed by controls that were not designed to support Commands. It allows you to map any arbitrary event on a control to a Command.
/// </summary>
public class EventToCommandBehavior : BaseBehavior<VisualElement>
{
// Omitted for brevity...
/// <inheritdoc/>
protected override void OnAttachedTo(VisualElement bindable)
{
base.OnAttachedTo(bindable);
RegisterEvent();
}
/// <inheritdoc/>
protected override void OnDetachingFrom(VisualElement bindable)
{
UnregisterEvent();
base.OnDetachingFrom(bindable);
}
static void OnEventNamePropertyChanged(BindableObject bindable, object oldValue, object newValue)
=> ((EventToCommandBehavior)bindable).RegisterEvent();
void RegisterEvent()
{
UnregisterEvent();
var eventName = EventName;
if (View is null || string.IsNullOrWhiteSpace(eventName))
{
return;
}
eventInfo = View.GetType()?.GetRuntimeEvent(eventName) ??
throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't resolve the event.", nameof(EventName));
ArgumentNullException.ThrowIfNull(eventInfo.EventHandlerType);
ArgumentNullException.ThrowIfNull(eventHandlerMethodInfo);
eventHandler = eventHandlerMethodInfo.CreateDelegate(eventInfo.EventHandlerType, this) ??
throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't create event handler.", nameof(EventName));
eventInfo.AddEventHandler(View, eventHandler);
}
void UnregisterEvent()
{
if (eventInfo is not null && eventHandler is not null)
{
eventInfo.RemoveEventHandler(View, eventHandler);
}
eventInfo = null;
eventHandler = null;
}
/// <summary>
/// Virtual method that executes when a Command is invoked
/// </summary>
/// <param name="sender"></param>
/// <param name="eventArgs"></param>
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
protected virtual void OnTriggerHandled(object? sender = null, object? eventArgs = null)
{
var parameter = CommandParameter
?? EventArgsConverter?.Convert(eventArgs, typeof(object), null, null);
var command = Command;
if (command?.CanExecute(parameter) ?? false)
{
command.Execute(parameter);
}
}
}
OnAttachedTo
および OnDetachingFrom
メソッドは、EventName
プロパティで定義されているイベントのイベント ハンドラーを登録および登録解除するために使用されます。 その後、イベントが発生したときに、コマンドを実行する OnTriggerHandled
メソッドが呼び出されます。
イベントが発生したときにコマンドを実行するために EventToCommandBehavior
を使用する利点は、コマンドとやりとりするように設計されていないコントロールにコマンドを関連付けられることです。 さらに、これにより、イベント処理コードがビュー モデルに移動され、そこで単体テストすることができます。
ビューからのビヘイビアーの呼び出し
EventToCommandBehavior
は、コマンドをサポートしていないコントロールにコマンドをアタッチする場合に特に便利です。 たとえば、LoginView では、次のコードに示すように、ユーザーが自分のパスワードの値を変更したときに ValidateCommand
を実行するために EventToCommandBehavior
を使用します。
<Entry
IsPassword="True"
Text="{Binding Password.Value, Mode=TwoWay}">
<!-- Omitted for brevity... -->
<Entry.Behaviors>
<mct:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding ValidateCommand}" />
</Entry.Behaviors>
<!-- Omitted for brevity... -->
</Entry>
実行時に、EventToCommandBehavior
によって、Entry
とのやりとりとの応答が行われます。 ユーザーが Entry
フィールドに入力すると、TextChanged
イベントが発生し、LoginViewModel
で ValidateCommand
が実行されます。 既定では、このイベントのイベント引数がコマンドに渡されます。 必要に応じて、EventArgsConverter
プロパティを使用して、イベントによって提供される EventArgs
を、コマンドで入力として想定される値に変換できます。
ビヘイビアーの詳細については、.NET MAUI デベロッパー センターの「ビヘイビアー」を参照してください。
まとめ
Model-View-ViewModel (MVVM) パターンは、アプリケーションのビジネスおよびプレゼンテーション ロジックをそのユーザー インターフェイス (UI) からクリーンに分離するのに役立ちます。 アプリケーション ロジックと UI の間でクリーンな分離を維持することは、多くの開発の問題に対処するのに役立ち、アプリケーションのテスト、維持、および進化が容易になります。 また、コード再利用の機会を大幅に向上させ、開発者や UI デザイナーがアプリのそれぞれの部分を開発するときに共同作業をより容易にすることができます。
MVVM パターンを使用して、アプリの UI と基になるプレゼンテーションおよびビジネス ロジックは、3 つの別のクラス (UI と UI ロジックをカプセル化するビュー、プレゼンテーション ロジックと状態をカプセル化するビュー モデル、アプリのビジネス ロジックとデータをカプセル化するモデル) に分けられます。
.NET