次の方法で共有


MVVM

ステート マシン パターンによる WPF コマンド処理

Tarquin Vaughan-Scott

Windows Presentation Foundation (WPF) には、UI とコマンド ロジックを分離できるようにする強力なコマンド処理フレームワークがあります。モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) デザイン パターンを使用すると、コマンドは ICommand インターフェイスを実装するビューモデルでプロパティとして公開されます。ビューのコントロールがこれらのプロパティにバインドされます。ユーザーがそのコントロールを操作すると、コマンドが実行されます。

当然のことながら、問題は細部にひそんでいます。真の課題は、コマンドを実行することにあるのではなく、その操作に対してビューモデルが有効な状態になっているときにコマンドが実行されるようにすることです。通常、「実行できるかどうか」の検証は、ローカル変数を使用して、IsLoading、CanDoXYZ、SomeObject != null などの条件式として実装します。新しい状態が追加されると、評価を必要とする条件が増えます。その結果、この方法はあっという間にかなり複雑になる可能性があります。

ステート マシンは特定の状態で許可される操作を制限するので、「実行できるかどうかを判断する」問題が解決されます。コマンドを直接ステート マシンにバインドすることが、この難しい問題の優れた解決策になります。

今回は、アプリケーションを分析して、ステート マシンを設計および実装する方法を示すほか、コマンドをステート マシンにバインドする方法についても説明します。また、非同期メソッドの再入を防ぐ場合、特定の状態においてコントロールを表示/非表示にする場合などにも別のメリットがあります。

ステート マシン

ステート マシンにはさまざまなバリエーションがありますが、本質は、状態間を遷移するプロセスを表すデザイン パターンです。ユーザー操作 (トリガー) がステート マシンの状態遷移のきっかけになります。状態ごとに許可する操作を制限するのがルールです。

有限ステート マシンは、ある時点で許可される状態を 1 つのみに制限します。階層型ステート マシンは状態とサブ状態を許可します。階層型ステート マシンでは、サブ状態に上位状態の属性がすべて継承されるため、一般的に実用性が高くなります。継承により、必要な構成の量を減らすことができます。

たとえば、ユーザーが [Search] ボタンをクリックした場合を考えてみます。このとき、アプリケーションは Searching 状態に遷移します。この状態にいるときは、ユーザーが他の操作 (キャンセル操作は除く場合があります) の実行を許可しません。そのため、Searching 状態には、キャンセル操作以外のすべての操作を防ぐ (無視する) ルールを設定します。また、一部のステート マシンの実装では、開始操作と終了操作を許可して、状態が開始および終了するときに、ルールのロジックが実行されるようにすることができます。

コマンド構造

コマンドは、ランプの魔人のように、主人の命令を実行するために存在します。しかし、コマンドとはそもそも何でしょうか。コマンドの起源は、コマンドライン インターフェイスです。コマンドライン インターフェイスにユーザーが命令を入力すると、アプリケーションがこの命令を解釈します。

この考え方は、最新のインターフェイスでは進化しており、ユーザーが目的とする操作が抽象化されます。たとえば、テキストをコピーする場合のコマンドは、[テキストのコピー] です。ユーザーは、マウスのボタンを右クリックしてメニュー項目をクリックするか、キーボード操作 (Ctrl+C キー) を使用してコピーを実行できます。コマンドの実装方法は、基盤となるアプリケーションや、構築の基となるフレームワークによって異なります。

WPF では、ICommand インターフェイスを通じて、コマンドの概念を実装します。このインターフェイスは、Microsoft .NET Framework の一部です。このインターフェイスには 2 つのメソッドと 1 つのイベントがあります。

  • void Execute(object parameter): コマンドが呼び出されたときにコードを実行します。
  • bool CanExecute(object parameter): コマンドが呼び出し可能かどうかを判断します。
  • event EventHandler CanExecuteChanged: CanExecute メソッドに影響する条件が変化したことをフレームワークに通知します。通常、WPF フレームワークによって処理されます。

基本的に、ボタンやメニュー項目などのコマンド ソースが ICommandSource インターフェイスを実装します。このインターフェイスには、ICommand 型の Command というプロパティがあります。このプロパティをビューモデルの ICommand 実装にバインドすることで、コントロールから Execute メソッドを呼び出すようになります。また、CanExecute メソッドの結果に応じて、このプロパティを有効または無効にします。コマンド ソースの役割をするコントロールに ICommand プロパティがない場合 (残念ながらよくあることです)、イベントをコマンドにする手法を使用できます。

.NET Framework に組み込まれている ICommand 実装は、RoutedCommand と RoutedUICommand です。これらは、イベントをビジュアル ツリーの上下にルーティングするので、MVVM パターンでの使用に適していません。Josh Smith の RelayCommand と、Prism フレームワークの DelegateCommand が MVVM パターンでの実装によく使用されます。

今回付属のコード サンプルには、ステート マシンの原則と、その原則を使用してコマンドを管理する方法を示す Visual Studio 2013 ソリューションを含めています。サンプル アプリケーションでは、ユーザーが従業員一覧を検索して従業員を選択し、その従業員の詳細を編集するダイアログを表示できます (図 1 参照)。

読み込みが完了し、編集可能な状態の従業員一覧
図 1 読み込みが完了し、編集可能な状態の従業員一覧

ステート マシンの設計

設計プロセスの最初の手順は、フロー チャート (状態ダイアグラム) を使用してアプリケーションのセクションを定義することです。図 2 に Employee Manager 画面のフロー チャート ダイアグラムを示します。ダイアグラムには、長時間実行される可能性のあるプロセスを示す Searching などのブロックが含まれています。これらのブロックを特別な方法で処理して、ビジー状態のときにユーザーが操作を再実行しないようにする必要があります。これらの媒介となる "ビジー" ブロックの作成は、.NET Framework の新しい非同期機能では特に重要です。制御がユーザーに戻されると、ユーザーが同じ操作の実行を再度試みて、再入が発生する可能性があります。

Employee Manager 画面のプロセスを表すフロー チャート
図 2 Employee Manager 画面のプロセスを表すフロー チャート

設計プロセスの 2 番目の手順として、ユーザーがアプリケーションを操作するコマンドを定義します。Employee Manager 画面から、ユーザーは検索、従業員の編集、編集の終了を操作できます。ユーザーは、従業員の選択と選択解除の操作も実行できますが、それらの処理はコマンドとして管理せず、DataGrid コントロールのデータ バインドとして処理します。コマンドは、図 2 に示すように、それぞれ関連するポイントのワークフローの矢印 (制御フロー) にマップすることになります。

ステート マシンの実装

ステート マシン フレームワークは、自由に利用できるライブラリが多数あるため、ゼロから作成する必要はありません。.NET Framework の唯一のオプションは、Workflow Foundation (WF) ステート マシン アクティビティです。これは、ここで解決しようとしている問題には複雑すぎますが、長時間実行される永続的なワークフローに非常に役立ちます。いくつか予備調査を行い、NuGet パッケージとして bit.ly/ZL58MG (英語) で公開されている、Stateless ステート マシン ライブラリを使用することにしました。

これで、設計フェーズで作成したフロー チャートを使用して、状態 (フロー チャートの各ブロック) とトリガー (フロー チャートの各矢印) の一覧を作成できるようになります。Stateless は状態とトリガーにジェネリック型を使用するので、これをそのまま使用して、両方に列挙値を使用します。

public enum States
{
  Start, Searching, SearchComplete, Selected, NoSelection, Editing
}
public enum Triggers
{
  Search, SearchFailed, SearchSucceeded, Select, DeSelect, Edit, EndEdit
}

列挙値を定義したら、fluent インターフェイスを使用して各状態を構成する必要があります。重要なオプションは次のとおりです。

  • SubstateOf(TState state): 上位状態があり、その構成のすべてを継承することを示します。
  • Permit(TTrigger trigger, TState targetState): トリガーによって対象の状態に遷移することを許可します。
  • Ignore(TTrigger trigger): トリガーが発生しても、そのトリガーを無視します。
  • OnEntry(Action entryAction): 状態に入るときにアクションを実行します。
  • OnExit(Action exitAction): 状態から出るときにアクションを実行します。

有効な構成が設定されていない状態でトリガーが発生すると、ステート マシンによって例外が発生します。以下は、Searching 状態の構成です。

Configure(States.Searching)
  .OnEntry(searchAction)
  .Permit(Triggers.SearchSucceeded, States.SearchComplete)
  .Permit(Triggers.SearchFailed, States.Start)
  .Ignore(Triggers.Select)
  .Ignore(Triggers.DeSelect);

OnEntry アクションで検索のプロセスが実行され、Permit で関連トリガーの発生が許可されます。DataGrid コントロールが基盤となるデータ ソースにバインドされている場合 (WPF のほとんどのリスト コントロールで行われる頭痛の種です) に、Ignore によってビューで Select トリガーと DeSelect トリガーを発生しないようになっています。

また、ステート マシンでは、2 つの重要なメソッドが公開されます。

  • void Fire(TTrigger): 以前の構成を使用してステート マシンを遷移させます。
  • bool CanFire(Trigger trigger): 現在の状態でトリガーの発生を許可している場合、true を返します。

これらが、コマンドを作成するために必要な主要メソッドです。これらが、実行と実行可能性の判断のロジックを実行します。

ステート マシンへのコマンドのバインド

MVVM パターンでは、ビューモデルに ICommand インターフェイスを実装するプロパティを公開します。このコマンド プロパティの作成は、Execute メソッドと CanExecute メソッドをステート マシンの Fire メソッドと CanFire メソッドにそれぞれバインドするだけの作業です。次のように拡張メソッドを作成して、このロジックを一元化します。

public static ICommand CreateCommand<TState, TTrigger>(
  this StateMachine<TState, TTrigger> stateMachine, TTrigger trigger)
    {
      return new RelayCommand
        (
          () => stateMachine.Fire(trigger),
          () => stateMachine.CanFire(trigger)
        );
    }

この拡張メソッドを作成したら、ビューモデルに ICommand プロパティを作成します (分析フェーズで特定したコマンドについては図 2 のフロー ダイアグラムをもう一度見てください)。

SearchCommand = StateMachine.CreateCommand(Triggers.Search);
EditCommand = StateMachine.CreateCommand(Triggers.Edit);
EndEditCommand = StateMachine.CreateCommand(Triggers.EndEdit);

ビューモデル コマンドへのビューのバインド

コマンド ソースの役割をするコントロールに対しては、ビューモデルの ICommand プロパティにバインドされた Command プロパティを用意できます。サンプル アプリケーションでは、次のように 2 つのボタンとメニュー項目をビューモデルのコマンド プロパティにバインドしています。

<Button ToolTip="Search" VerticalAlignment="Center" 
  Style="{StaticResource ButtonStyle}"
  Command="{Binding SearchCommand}">
  <Image Source="Images\Search.png"></Image>
</Button>

これで、コマンドがステート マシンに直接バインドされるようになります。コマンドが実行されると、構成したトリガーが起動します。ここで何より重要なのは、現在の状態でトリガーが許可されていない場合は、コマンドが無効になることです。これにより、難しい実行可能ロジックが解決します。また、すべてステート マシン内で整然と構成されています。図 3 は、検索でビジー状態になっている Employee Manager 画面を示しています。Searching 状態では Search トリガーが許可されていないため、[Search] コマンドが無効になっているのがわかります。

検索ダイアログのビジー アニメーション
図 3 検索ダイアログのビジー アニメーション

その他のメリット

ステート マシンを実装したら、他の視覚要素をその状態にバインドすることもできます。検索機能が実行中でビジー状態の場合 (非同期メソッド内の長時間実行処理でもこの状態になることがあります)、ユーザーにビジー状態インジケーターを表示するのが理想的です。図 3 は、ステート マシンが Searching 状態のときにのみ表示するアニメーション画像です。この処理は、次のように、ステート マシンの状態を Visibility の値に変換するカスタム コンバーターを作成して実現します。

public class StateMachineVisibilityConverter : IValueConverter
  {
    public object Convert(object value, Type targetType,
      object parameter, CultureInfo culture)
    {
      string state = value != null ? 
        value.ToString() : String.Empty;
      string targetState = parameter.ToString();
      return state == targetState ? 
        Visibility.Visible : Visibility.Collapsed;
    }
 }

この後、次のようにカスタム コンバーターを使用してアニメーション画像をステート マシンにバインドします。

<local:AnimatedGIFControl Visibility="{Binding StateMachine.State,
                          Converter={StaticResource StateMachineConverter},
                          ConverterParameter=Searching}"/>

同じ原則を、Employee Manager 画面上に表示される編集ダイアログ (図 4) に当てはめることができます。ステート マシンが Editing 状態の場合、このダイアログが表示され、ユーザーは選択した従業員の詳細を編集できます。

ダイアログの編集状態
図 4 ダイアログの編集状態

気持ちの問題

ステート マシン パターンによって、コマンド ロジックに関する問題が解決するだけでなく、詳細なアプリケーション分析を行おうという気持ちも生まれます。コマンドを、ある種のロジックを実行するコントロールと考えることは簡単です。見逃されることが多いのは、コマンドの実行を許可する条件の判断です。コマンド ステート マシン パターンによって、設計プロセスの早い段階でこれらの問題に対処できます。これらは、通常、ステート マシン構成の一部です。ステート マシンによって、ビューモデルのコードが簡略化されます。皆さんのアプリケーションでも、同じようにすることができます。

その他のリソース

  • ステート マシン: ステート マシンについての優れた解説を探すのは非常に難しい作業ですが、ゲーム開発者の Bob Nystrom がうまくまとめています (bit.ly/1uGxVv6、英語)。
  • WPF MVVM: WPF-MVVM の "父" Josh Smith が、MSDN マガジンの 2009 年 2 月号 (msdn.microsoft.com/magazine/dd419663) で、ICommand インターフェイスである RelayCommand の実装を説明しています。
  • コマンド: WPF コマンド実行に関する MSDN 公式ドキュメントに、.NET Framework の一部であるルーティング コマンドとルーティング イベントの説明があります (msdn.microsoft.com/ja-jp/library/ms752308(v=vs.110).aspx)。
  • ルーティング コマンド: MSDN マガジンの 2008 年 9 月号で、Brian Noyes がルーティング コマンドについて説明しています。ルーティング コマンドと MVVM コマンド パターンの違いについて理解するためのよい出発点となります (msdn.microsoft.com/ja-jp/magazine/cc785480.aspx#id0190070)。
  • Command、RelayCommand、および EventToCommand: 2013 年 5 月で、Laurent Bugnion がイベントをコマンドに変換する方法を説明しています。コマンドへの変換は、ICommandSource インターフェイスを実装していないコントロールに役立ちます (msdn.microsoft.com/magazine/dn237302、英語)。
  • Prism フレームワーク: Prism フレームワークの公式ドキュメントで、MVVM パターンと DelegateCommand が説明されています (msdn.microsoft.com/ja-jp/library/gg405484(v=pandp.40).aspx、英語)。
  • NuGet Stateless パッケージ: NuGet Web サイトの Stateless パッケージに、ダウンロードの説明が付属しています (bit.ly/1sXBQl2、英語)。

Tarquin Vaughan-Scott は、南アフリカのケープ タウンに拠点を置き、金融業界向けのデータ管理ソリューションを作成している InfoVest (infovest.co.za) の主任開発者です。興味のある分野はデータベースの設計とアーキテクチャで、特に、トランザクション システムとデータ ウェアハウス システムの違いに興味があります。連絡先は tarquin@infovest.co.za (英語のみ) です。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Nicholas Blumhardt に心より感謝いたします。
Nicholas Blumhardt (Stateless) は、MEF チームに所属していたマイクロソフトの元従業員です。