高度な WPF
WPF におけるルーティング イベントとルーティング コマンドについて
Brian Noyes
この記事では、次の内容について説明します。
- イベントのルーティングとビジュアル ツリー
- コマンドのルーティング
- コマンドのルーティングに関する問題を回避する
- ルーティング コマンドの先を行く
|
この記事では、次のテクノロジを使用しています。
WPF
|

目次
Windows® Presentation Foundation (WPF) の進歩に追いつこうとするとき、最大の試練の 1 つが、非常に多くの新しい構成要素を習得しなければならないということです。Microsoft® .NET Framework のプロパティやイベントなどは単純な要素ですが、WPF ではこれらに相当する要素にも新たな機能が追加されており、それに伴って複雑さも増しています (特に依存関係プロパティとルーティング イベント)。さらに、アニメーション、スタイル設定、コントロール テンプレート、ルーティング コマンドなど、まったく新しい要素もあります。学ぶべきことは山積みです。
この記事では、習得する必要のある新しい WPF 要素のうち、非常に重要な 2 つのアイテムについて重点的に説明します。これらのアイテム (ルーティング イベントとルーティング コマンド) は相互に関連しています。これらは、ユーザー インターフェイスのさまざまなパーツ間での対話の基盤となります。それらのパーツが 1 つの大きな Window クラスに含まれる個別のコントロールであるか、ユーザー インターフェイスの独立した別個のパーツに含まれるコントロールとそのサポート コードであるかは問いません。この記事の内容は、組み込みの WPF コントロールを使用し、UI のレイアウトを XAML で宣言して UI を構築する方法など、WPF の基本を理解していることを前提にしています。
ルーティング イベントの概要
WPF を使い始めたばかりの開発者は、無意識にルーティング イベントを使用していることがあります。たとえば、Visual Studio® デザイナでウィンドウにボタンを追加し、それに myButton という名前を付けたとします。ボタンをダブルクリックすると、Click イベントが XAML マークアップにフックされ、Click イベントのイベント ハンドラが Window クラスの分離コードに追加されます。このしくみは、Windows フォームおよび ASP.NET でのイベントのフックと変わりがないように思われるでしょう。確かに ASP.NET のコーディング モデルにも若干似ていますが、むしろ Windows フォームのランタイム モデルに近いモデルと言えます。具体的には、ボタンの XAML マークアップのコードは次のようになります。
<Button Name="myButton" Click="myButton_Click">Click Me</Button>
イベントをフックするための XAML 宣言は XAML でのプロパティの割り当てに似ていますが、イベント ハンドラを指定したオブジェクトでの通常のイベント フックアップとなります。このフックアップは、実際にはコンパイル時に生成されるウィンドウの部分クラスで行われます。それを確認するには、クラスのコンストラクタに移動し、InitializeComponent メソッドの呼び出しを右クリックします。次に、コンテキスト メニューから [定義へ移動] を選択します。通常はコンパイル時に生成されるコードが含まれる、生成済みのコード ファイル (名前付け規則により、.i.g.cs または .i.g.vb が使用されています) がエディタに表示されます。表示された部分クラスを Connect メソッドまで下方向にスクロールし、次の部分を確認してください。
#line 6 "..\..\Window1.xaml"
this.myButton.Click +=
new System.Windows.RoutedEventHandler(
this.myButton_Click);
この部分クラスはコンパイル時に XAML から生成され、デザイン時にコンパイルする必要のある XAML の要素が含まれます。XAML のほとんどはコンパイル済みのアセンブリにバイナリ リソースとして埋め込まれ、実行時にマークアップのバイナリ表現のコンパイル済みコードとマージされます。
ウィンドウの分離コードを見ると、Click ハンドラは次のようになっています。
private void myButton_Click(
object sender, RoutedEventArgs e) { }
ここまでのところ、イベント フックアップは他の .NET イベント フックアップとよく似ています。つまり、オブジェクト上のイベントにフックされたデリゲートが明示的に宣言されており、そのデリゲートは処理メソッドをポイントしています。使用しているのがルーティング イベントであることを示しているのは、Click イベントのイベント引数の型 (RoutedEventArgs) だけです。それでは、ルーティング イベントのどのような点がそれほど特殊なのでしょうか。それを理解するには、まず WPF の要素の構成モデルについて理解する必要があります。
WPF 要素のツリー
プロジェクトでウィンドウを新たに作成し、デザイナでボタンをウィンドウにドラッグすると、XAML の要素ツリーは次のような構造になります (わかりやすいように属性は省略しています)。
<Window>
<Grid>
<Button/>
</Grid>
</Window>
これらの各要素は対応する .NET 型のランタイム インスタンスを表し、宣言された要素の階層は論理ツリーと呼ばれる構造になります。また、WPF の多くのコントロールは ContentControls と ItemsControls のどちらかなので、子要素を持つことができます。たとえば、Button は ContentControl なので、複雑な子要素をコンテンツとして含めることができます。論理ツリーは次のように拡張できます。
<Window>
<Grid>
<Button>
<StackPanel>
<Image/>
<TextBlock/>
</StackPanel>
</Button>
</Grid>
</Window>
このツリーは、図 1 に示すような UI になります。
図 1 ボタン コンテンツを持つシンプルなウィンドウ
ご想像どおり、ツリーはこの後さまざまな形にカスタマイズできます (Grid に別の Button を含めるなど)。それに伴って、論理ツリーは大幅に複雑化する可能性があります。論理ツリーの WPF 要素に関して理解しておかなければならないのは、現在の構造は実行時に別の形になるということです。通常、それらの各要素は、実行時により複雑なビジュアル要素ツリーに展開されます。この例では、要素の論理ツリーは図 2 に示す要素のビジュアル ツリーに展開されます。
図 2 シンプルなウィンドウのビジュアル ツリー
私は、
図 2 に示すビジュアル ツリーの要素を確認するために、Snoop (
blois.us/Snoop) というツールを使用しました。ウィンドウ (EventsWindow) のコンテンツは実際には Border と AdornerDecorator にラップされており、その内部のコンテンツは ContentPresenter によって表示されていることがわかります。ボタンも同様で、ボタンのコンテンツは ButtonChrome オブジェクトにラップされており、コンテンツは ContentPresenter によって表示されます。
ボタンをクリックするとき、実際にクリックしているのは Button 要素ではなくビジュアル ツリーの子要素であることもあります。この要素は、ButtonChrome など、上記の論理ツリーには含まれていない要素である場合もあります。たとえば、ボタン内のイメージ上でマウスをクリックするとします。当初、クリック イベントは Image 要素内の MouseLeftButtonDown イベントとして生成されますが、何らかの方法で Button レベルの Click イベントに変換されなければなりません。ここで役立つのが、ルーティング イベントのルーティング機能です。
イベントのルーティング
ルーティング イベントは主にビジュアル ツリーに基づいてルーティングされるため、論理ツリーとビジュアル ツリーについて多少の知識を持っていることが重要です。ルーティング イベントは、Bubble、Tunnel、Direct の RoutingStrategy をサポートしています。
最も一般的なのが Bubble です。イベントは、処理されるまで、またはビジュアル ツリーのルート要素に到達するまで、ソース要素からバブルアップ (伝播) されます。これにより、オブジェクト上のイベントをソース要素よりも上の階層で処理できるようになります。たとえば、Button.Click ハンドラをボタン自体に直接アタッチする代わりに、ボタンを囲む Grid 要素にアタッチできます。Bubble イベントの名前は、そのアクションを表しています (MouseDown など)。
Tunnel イベントは、処理されるまで、またはイベントのソース要素に到達するまで、逆の方向に (ルート要素から要素ツリーの下方向に) ルーティングされます。これにより、上層の要素でイベントをインターセプトし、ソース要素にイベントが到達する前に処理することができます。Tunnel イベントの名前には、原則として Preview というプレフィックスが付きます (PreviewMouseDown など)。
Direct イベントの動作は、.NET Framework の通常のイベントと同様です。イベントのハンドラとして使用できるのは、イベントにフックされるデリゲートだけです。
Tunnel イベントが特定のイベントに対して定義されている場合、通常は対応する Bubble イベントが存在します。その場合、Tunnel イベントが最初に発生し、ハンドラを見つけるためにルート要素からソース要素に向かって下方向にルーティングされます。Tunnel イベントが処理されるか、ソース要素に到達すると、Bubble イベントが発生し、ハンドラを見つけるためにソース要素から上方向にルーティングされます。Bubble イベントまたは Tunnel イベントのルーティングは、イベント ハンドラが呼び出されただけでは停止しません。バブリング プロセスまたはトンネリング プロセスを停止するには、渡されるイベント引数を使用して、イベント ハンドラでイベントに処理済みとしてマークする必要があります。
private void OnChildElementMouseDown(object sender,
MouseButtonEventArgs e) {
e.Handled = true;
}
ハンドラによって処理済みとしてマークされたイベントは、他のハンドラに渡されることはありません。ただし、例外もあります。実際にはイベントのルーティングは背後で続いているため、UIElement.AddHandler メソッドのオーバーライドでコードにイベント ハンドラを明示的にフックできます。この UIElement.AddHandler メソッドには、イベントが処理済みとしてマークされていてもハンドラが呼び出されるように指定できる追加のフラグがあります。そのフラグは、次のような呼び出しで指定します。
m_SomeChildElement.AddHandler(UIElement.MouseDownEvent,
(RoutedEventHandler)OnMouseDownCallMeAlways,true);
AddHandler の最初のパラメータは、処理対象の RoutedEvent です。2 番目のパラメータは、イベント処理メソッド (イベントのデリゲートの適切なシグネチャが必要) へのデリゲートです。3 番目のパラメータでは、別のハンドラによってイベントが処理済みとしてマークされている場合も通知が行われるようにするかどうかを指定します。AddHandler は、ルーティング中にイベントのフローを監視する要素で呼び出します。
ルーティング イベントと複合
ここからは、Button.Click イベントの発生のしくみについて説明し、それがなぜ重要なのかを明らかにします。既に説明したように、Click イベントは、Button のビジュアル ツリーに含まれている子要素 (前述の例の Image など) 上の MouseLeftButtonDown イベントを元に生成されます。
MouseLeftButtonDown イベントが Image 要素内で発生すると、PreviewMouseLeftButtonDown イベントがルートから Image まで下方向にルーティングされます。ハンドラによってプレビュー イベントの Handled フラグが true に設定されなかった場合は、MouseLeftButtonDown イベントは Image 要素から Button までバブルアップされます。ボタンはそのイベントを処理し、Handled フラグを true に設定します。そしてボタン自体の Click イベントを生成します。このプロセスを視覚化できるように、この記事のサンプル コードにはルーティング チェーン全体でハンドラをフックしたアプリケーションが含まれています。
こうしたしくみには非常に重要な意味があります。たとえば、Ellipse 要素の含まれるコントロール テンプレートを適用して既定のボタンの外観を置き換えた場合、Ellipse の外部がクリックされたときに Click イベントが発生しないように、何か特別な処理を行う必要はありません。Ellipse の境界線のすぐ外側をクリックすると、ボタンの長方形の境界線内でクリックしたことになります。Ellipse 自体は MouseLeftButtonDown を検出できますが、ボタンの境界線と Ellipse の間の何もない部分ではこのイベントを検出できません。
そのため、Ellipse の内部をクリックした場合にのみ MouseLeftButtonDown イベントが発生します。このイベントはテンプレートがアタッチされている Button クラスによって引き続き処理されるので、カスタマイズされているボタンの動作も予測可能です。この概念は、独自のカスタム複合コントロールを記述する際にもきわめて重要です。コントロール内部に配置された子要素のイベントを処理するために Button で行っているような処理は、さまざまな場面で必要になるためです。
アタッチされるイベント
ある要素で宣言されているイベントを別の要素でも処理できるように、WPF はアタッチされるイベントと呼ばれるイベントをサポートしています。アタッチされるイベントは、イベントが宣言されている型以外の要素の XAML におけるフックアップをサポートするルーティング イベントです。たとえば、Grid 要素で Button.Click イベントをリッスンしてバブルアップするには、単純に次のようにフックします。
<Grid Button.Click="myButton_Click">
<Button Name="myButton" >Click Me</Button>
</Grid>
この場合、コンパイル時に生成される部分クラスのコードは次のようになります。
#line 5 "..\..\Window1.xaml"
((System.Windows.Controls.Grid)(target)).AddHandler(
System.Windows.Controls.Primitives.ButtonBase.ClickEvent,
new System.Windows.RoutedEventHandler(this.myButton_Click));
アタッチされるイベントを使用することで、イベント ハンドラをフックする場所をいくらか柔軟に指定できるようになります。ただし、この例のように要素が同じクラスに含まれている場合は、どのような違いが生じるかがはっきりしない可能性もあります。どちらの場合も処理メソッドは Window クラス上のメソッドであるためです。
アタッチされるイベントについては、2 つの点に気を付ける必要があります。1 つ目は、イベント ハンドラはバブリングまたはトンネリング要素チェーンにおける処理要素の位置に基づいて呼び出されるという点です。2 つ目は、使用しているコントロールの内部にカプセル化されているオブジェクトからのイベントの処理が可能になるという点です。たとえば、Grid で Button.Click などのイベントを処理できますが、それらの Button.Click イベントは、ウィンドウに含まれるユーザー コントロールの内部からバブルアウトされている可能性があります。
あらゆるイベントがアタッチされるイベントとして宣言されるわけではありません。事実、アタッチされるイベントとして宣言されるイベントはごくわずかです。ただし、アタッチされるイベントは、コントロールのソース以外の場所でイベント処理を行う必要がある場合に非常に便利です。
ルーティング コマンドの概要
ルーティング イベントについて理解したところで、ルーティング コマンドに進みましょう。WPF ルーティング コマンドは、繰り返し使用される多数の密結合コードをアプリケーションに組み込むことなく、ツール バー ボタンやメニュー項目などの UI コントロールをハンドラにフックするための特殊なメカニズムを提供します。ルーティング コマンドには、通常のイベント処理に加えて、3 つの主要な特徴があります。
- ルーティング コマンドのソース要素 (呼び出し元) とコマンド ターゲット (ハンドラ) とを切り離すことができます。つまり、イベント ハンドラで双方がリンクされている場合には必要となる直接参照が、ここでは不要になります。
- ハンドラによってコマンドが無効であることが示されると、ルーティング コマンドは関連付けられているすべての UI コントロールを自動的に有効または無効にします。
- ルーティング コマンドを使用すると、キーボード ショートカットやその他の形の入力ジェスチャ (インク ジェスチャなど) を、コマンドを呼び出すための新たな方法として関連付けることができます。
さらに、ルーティング コマンドに特徴的な RoutedUICommand クラスでは、コマンドの呼び出し元であるコントロールのテキスト表示に使用される、単一の Text プロパティを定義できます。Text プロパティのローカライズは、関連付けられている各呼び出し元コントロールへのアクセスよりも簡単です。
呼び出し元でコマンドを宣言するには、単純に、コマンドを呼び出すコントロールで Command プロパティを設定します。
<Button Command="ApplicationCommands.Save">Save</Button>
Command プロパティは、MenuItem、Button、RadioButton、CheckBox、Hyperlink、およびその他多数のコントロールでサポートされています。
コマンド ハンドラとして動作させる要素に CommandBinding を設定してください。
<UserControl ...>
<UserControl.CommandBindings>
<CommandBinding Command="ApplicationCommands.Save"
CanExecute="OnCanExecute" Executed="OnExecute"/>
</UserControl.CommandBindings>
...
</UserControl>
コマンド バインディングの CanExecute プロパティと Executed プロパティは、コマンド処理プロセス中に呼び出されるメソッド (宣言するクラスの分離コードに含まれる) をポイントしています。ここで重要なのは、コマンドの呼び出し元でコマンド ハンドラへの認識、つまりコマンド ハンドラへの参照を行う必要がないこと、そして、どのコマンドを呼び出すかをハンドラで認識しなくてもよいということです。
CanExecute は、コマンドを有効にするかどうかを決定するために呼び出されます。コマンドを有効にするには、次に示すようにイベント引数の CanExecute プロパティを true に設定します。
private void OnCanExecute(object sender,
CanExecuteRoutedEventArgs e) {
e.CanExecute = true;
}
また、Executed メソッドが定義済みで CanExecute メソッドを含まないコマンド ハンドラが存在する場合にも、コマンドは有効になります (その場合、CanExecute は暗黙的に true)。Executed メソッドは、呼び出されるコマンドに基づいて適切な操作を受け取ります。これには、ドキュメントの保存、注文の送信、電子メールの送信など、コマンドが関連付けられている操作が含まれます。
ルーティング コマンドの動作
ルーティング コマンドの動作をより明確にし、そのメリットを簡単に理解できるように、単純な例を挙げて説明します。図 3 に、2 つの入力用テキスト ボックスと、テキスト ボックス内のテキストに対して Cut (切り取り) 操作を実行するためのツール バー ボタンを備えた、シンプルな UI を示します。
図 3 Cut (切り取り) コマンド ツール バー ボタンを備えたシンプルなアプリケーション
イベントを使用してこれをフックするには、ツール バー ボタンの Click ハンドラを定義する必要があります。また、そのコードには 2 つのテキスト ボックスへの参照が必要です。どちらのテキスト ボックスにフォーカスがあるかを特定し、コントロール内で選択されたテキストに基づいて適切なクリップボード操作を呼び出す必要があります。さらに、フォーカスのある場所や、テキスト ボックスでテキストが選択されているかどうかに基づいて、ツール バー ボタンを適切なタイミングで有効および無効にすることも考慮しなければなりません。これらを実現するには、わかりづらく煩雑な密結合コードが必要になります。
このようなシンプルなフォームの場合はそれほど面倒には思えませんが、対象のテキスト ボックスがユーザー コントロールまたはカスタム コントロールの内部にあり、ウィンドウの分離コードがそれらに直接アクセスできない場合はどうしたらよいでしょうか。ユーザー コントロールの境界で API を公開してコンテナから要素をフックできるようにするか、テキスト ボックスをパブリックに公開する必要があります。しかし、どちらのアプローチも理想的とは言えません。
コマンドを使用すれば、ツール バー ボタンの Command プロパティを WPF で定義されている Cut コマンドに設定するだけで済みます。これで作業は完了です。
<ToolBar DockPanel.Dock="Top" Height="25">
<Button Command="ApplicationCommands.Cut">
<Image Source="cut.png"/>
</Button>
</ToolBar>
これでアプリケーションを実行できるようになりました。ツール バー ボタンは当初は無効になっていることがわかります。どちらかのテキスト ボックスでテキストを選択すると、ツール バー ボタンが有効になります。そのボタンをクリックすると、実際にテキストが切り取られ、クリップボードにコピーされます。UI のどの部分に位置するテキスト ボックスでも、同様の動作が可能です。なかなか良いと思いませんか。
TextBox クラスの実装には Cut コマンドのコマンド バインディングが組み込まれていて、そのコマンドのクリップボード処理が自動的にカプセル化されています (Copy や Paste の場合も同様)。それでは、フォーカスのあるテキスト ボックスだけでコマンドが呼び出されるようにするにはどうすればよいでしょうか。また、コマンドの処理を指示するメッセージはどのような方法でテキスト ボックスに伝達すればよいのでしょうか。ここで活躍するのが、ルーティング コマンドのルーティング機能です。
コマンドのルーティング
ルーティング コマンドとルーティング イベントの違いは、コマンドの呼び出し元からコマンド ハンドラへのコマンドのルーティング方法にあります。具体的に言うと、ルーティング イベントは、(ビジュアル ツリーにフックされたコマンド バインディングを通じて) メッセージをコマンドの呼び出し元とコマンド ハンドラの間でルーティングするために背後で使用されます。
この場合、多対多のリレーションシップが存在する可能性もありますが、ある時点で実際にアクティブなコマンド ハンドラは 1 つだけです。アクティブなコマンド ハンドラは、コマンドの呼び出し元とコマンド ハンドラがビジュアル ツリーのどこにあるか、およびフォーカスが UI のどの部分にあるかによって特定されます。ルーティング イベントにより、アクティブなコマンド ハンドラを呼び出してコマンドを有効にするかどうかをたずねると共に、コマンド ハンドラの Executed メソッドのハンドラを呼び出します。
通常、コマンドの呼び出し元は、ビジュアル ツリーにおけるそれ自体の位置とビジュアル ツリーのルートの間でコマンド バインディングを探します。コマンド バインディングが見つかった場合、バインドされているコマンド ハンドラは、コマンドが有効であるかどうかを判断し、コマンドが呼び出されたときに呼び出されます。コマンドがツール バーまたはメニュー内 (より一般的に言うと、FocusManager.IsFocusScope を true に設定するコンテナ) のコントロールにフックされている場合は、追加のロジックが実行され、そのロジックがビジュアル ツリー パスのルートからフォーカス要素に向かってコマンド バインディングを探します。
図 3 のシンプルなアプリケーションの場合、Cut (切り取り) コマンド ボタンはツール バーに配置されているため、CanExecute と Execute はフォーカスのある TextBox インスタンスによって処理されます。図 3 のテキスト ボックスがユーザー コントロール内に配置されている場合は、ウィンドウ、Grid を含むユーザー コントロール、テキスト ボックスを含む Grid、または個別のテキスト ボックスにコマンド バインディングを設定できます。どのテキスト ボックスにフォーカスがあるかによって、ルートを起点とするフォーカス パスの終了位置が決まります。
WPF のルーティング コマンドのルーティングについて理解するうえで重要なのは、1 つのコマンド ハンドラが呼び出されたら、その他のハンドラは呼び出されないということです。そのため、ユーザー コントロールで CanExecute メソッドを処理する場合は、TextBox の CanExecute の実装は呼び出されません。
コマンドを定義する
ApplicationCommands.Save コマンドと ApplicationCommands.Cut コマンドは、WPF が提供する数多くのコマンドのうちの 2 つです。WPF の 5 つの組み込みコマンド クラスとそれらに含まれるコマンドの一部を図 4 に示します。

図 4 WPF のコマンド クラス
| コマンド クラス |
コマンドの例 |
| ApplicationCommands |
Close、Cut、Copy、Paste、Save、Print |
| NavigationCommands |
BrowseForward、BrowseBack、Zoom、Search |
| EditingCommands |
AlignXXX、MoveXXX、SelectXXX |
| MediaCommands |
Play、Pause、NextTrack、IncreaseVolume、Record、Stop |
| ComponentCommands |
MoveXXX、SelectXXX、ScrollXXX、ExtendSelectionXXX |
XXX は、MoveNext、MovePrevious などの操作のコレクションを示します。各クラスのコマンドはパブリックな静的プロパティ (Visual Basic® の Shared プロパティ) として定義されているため、簡単にフックできます。独自のカスタム コマンドも同じアプローチで簡単に定義することができます。その例は後で示します。
これらのコマンドは、次のような簡便な表記法で使用することもできます。
<Button Command="Save">Save</Button>
この短縮バージョンを使用した場合、WPF の型変換によって組み込みコマンドのコレクションに対して名前付きコマンドの検索が試行されます。最終的にはこの場合もまったく同じ結果になります。私自身は、コードをわかりやすく、保守しやすくするために、長い名前のバージョンを使うようにしています。そうすると、そのコマンドがどこに定義されているかもはっきりします。これらは組み込みコマンドですが、EditingCommands クラスと ComponentCommands クラスで一部が重複しています。
コマンドの組み込み
ルーティング コマンドは、WPF で定義されている ICommand インターフェイスの特殊な実装です。ICommand の定義は次のとおりです。
public interface ICommand {
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
組み込みの WPF コマンド型は、RoutedCommand と RoutedUICommand です。これらのクラスはどちらも ICommand インターフェイスを実装し、前述のルーティング イベントを使用してルーティングを行います。
コマンドの呼び出し元は CanExecute を呼び出し、関連付けられているコマンド呼び出しコードを有効にするか、無効にするかを決定します。コマンドの呼び出し元は、CanExecuteChanged イベントをサブスクライブすることで、メソッドを呼び出すタイミングを判断します。RoutedCommand クラスの CanExecuteChanged は、UI の状態またはフォーカスの変化に応じて発生します。コマンドが呼び出されると、Executed メソッドが呼び出され、ビジュアル ツリーに沿ってルーティングされるルーティング イベントを通じてハンドラにディスパッチされます。
Command プロパティをサポートするクラス (ButtonBase クラスなど) は、ICommandSource インターフェイスを実装します。
public interface ICommandSource {
ICommand Command { get; }
object CommandParameter { get; }
IInputElement CommandTarget { get; }
}
Command プロパティは、呼び出し元とその呼び出し元が呼び出すコマンドを関連付けます。CommandParameter によって、呼び出し元はコマンドの呼び出しと共にデータを渡せるようになります。CommandTarget プロパティを使用すると、フォーカス パスに基づいて既定のルーティングをオーバーライドし、指定した要素をコマンドのハンドラとして使用するようにコマンド実行システムに指示できます。したがって、ルーティング イベントや、フォーカスに基づいたコマンド ハンドラの特定に頼らなくても済みます。
ルーティング コマンドに関する課題
ルーティング コマンドは、シンプルなユーザー インターフェイスのシナリオ、ツール バーおよびメニュー項目のフック、必然的にキーボード フォーカスと連動するアクション (クリップボード操作など) の処理に役立ちます。ただし、複雑なユーザー インターフェイスを構築する場合、コマンド処理ロジックがビュー定義のサポート コードに含まれている場合、コマンドの呼び出し元が必ずしもツール バーまたはメニューに含まれるとは限らない場合は、ルーティング コマンドでは力不足です。こうした状況は、Model View Controller (MVC (
msdn.microsoft.com/magazine/cc337884))、Model View Presenter (MVP (
msdn.microsoft.com/magazine/cc188690))、WPF のコミュニティで Model View ViewModel とも呼ばれているプレゼンテーション モデル (
msdn.microsoft.com/library/cc707885) などの UI 構成パターンを使用する際によく生じます。
この領域に踏み込んだ際に問題となるのは、コマンドの有効化ロジックと処理ロジックがビジュアル ツリーに直接含まれておらず、プレゼンタ モデルまたはプレゼンテーション モデルに含まれている場合があるということです。また、コマンドを有効にするかどうかを決める状態は、ビジュアル ツリーにおけるコマンドの呼び出し元およびビューの位置とは関係がない可能性もあります。ある時点で特定のコマンドに対して有効なハンドラが複数存在するようなシナリオも考えられます。
どのような場面でルーティング コマンドに関する問題が生じるのかを図 5 を使用して説明します。このシンプルなウィンドウには、1 組のユーザー コントロールが含まれています。これらは、MVP または MVC パターンのビューです。メイン ウィンドウには、[Save] (保存) コマンドの含まれる [File] (ファイル) メニューとツール バーがあります。メイン ウィンドウの上部には、入力用テキスト ボックスと、Command が Save コマンドに設定されているボタンもあります。
図 5 複合ユーザー インターフェイス (クリックすると拡大画像が表示されます)
残りの UI は 2 つのビューによって提供されます。これらのビューは、それぞれがシンプルなユーザー コントロールのインスタンスです。各ユーザー コントロール インスタンスが UI のどの部分を提供しているかをわかりやすくするため、ユーザー コントロール インスタンスの境界線にはぞれぞれ異なる色が設定されています。各ユーザー コントロール インスタンスには、Command プロパティが Save コマンドに設定されている [Save] (保存) ボタンがあります。
ビジュアル ツリーでの位置に深く関係するルーティング コマンドに伴う課題は、このシンプルな例で明らかにできます。図 5 のウィンドウ自体には、Save コマンドの CommandBinding はありません。ただし、そのコマンドの 2 つの呼び出し元 (メニューとツール バー) が含まれています。この場合、トップレベル ウィンドウには、コマンドが呼び出された際に実行する処理を把握させる必要はありません。コマンドの処理は、ユーザー コントロールで表される子のビューに任せます。この例のユーザー コントロール クラスには Save コマンドの CommandBinding があり、その CommandBinding は CanExecute = true を返します。
ただし、図 5 では、フォーカスがウィンドウ レベルの上部テキスト ボックスにあるとき、そのレベルのコマンドの呼び出し元が無効になっていることにお気付きでしょう。また、フォーカスがユーザー コントロール内にないのに、そのユーザー コントロールの [Save] (保存) ボタンが有効になっています。
一方のユーザー コントロール インスタンス内のどちらかのテキスト ボックスにフォーカスを移すと、メニューとツール バーのコマンド呼び出し元が有効になります。ただし、ウィンドウ自体にある [Save] (保存) ボタンは有効になりません。実際にこの場合は、ウィンドウ上部のテキスト ボックスの横にある [Save] (保存) ボタンを通常のルーティングで有効にする手立てはありません。
その理由は、やはり個別のコントロールの位置にあります。コマンド ハンドラがウィンドウ レベルになく、フォーカスがユーザー コントロールの外部にある間、コマンドの呼び出し元としてフックされているコントロールを有効にするコマンド ハンドラは、ビジュアル ツリーの上層にもフォーカス パス上にも存在しません。したがって、それらのコントロールのコマンドは既定で無効になります。一方、ユーザー コントロール内のコマンドの呼び出し元はどうかというと、ビジュアル ツリーの上層にハンドラがあるため、コマンドは有効になります。
フォーカスをどちらかのユーザー コントロール内のテキスト ボックスに移すと、ウィンドウとフォーカス パス上のテキスト ボックスの間にあるユーザー コントロールによって、ツール バーとメニューのコマンドを有効にするコマンド ハンドラが提供されます。フォーカス パスだけでなく、ビジュアル ツリーのテキスト ボックスからルートまでのパスがチェックされるためです。また、ウィンドウ レベルのボタンとルートの間にハンドラが配置されることはないため、ウィンドウ レベルのボタンを有効にすることはできません。
上記の簡単な例でさえ、この難解なビジュアル ツリーとフォーカス パスのしくみに頭を悩ませているとしたら、コマンドの呼び出し元とハンドラがビジュアル ツリーのさまざまな場所に位置する非常に複雑な UI でコマンドの有効化や呼び出しを行うとしたらどうなるでしょうか。頭が破裂しそうですね。映画「スキャナーズ」のワンシーンが浮かんでくるようです。
コマンドの問題を回避する
ルーティング コマンドとビジュアル ツリーの位置に関係する問題を回避するには、シンプルな状態を保つ必要があります。通常は、コマンドを呼び出す要素か、ビジュアル ツリーでその要素よりも上層にある要素に、コマンド ハンドラを配置しなければなりません。そのための 1 つの方法が、コマンド ハンドラを提供するコントロールの CommandManager.RegisterClassCommandBinding メソッドを使用してコマンド バインディングをウィンドウ レベルに挿入する方法です。
ただし、それ自体がキーボード フォーカスを受け入れるカスタム コントロール (テキスト ボックスなど) を実装する場合は例外です。その場合は、コマンド処理をコントロール自体に組み込み、フォーカスがコントロールにあるときにだけそのコマンド処理が機能するようにすると、前述の Cut コマンドの例のようにうまく機能します。
フォーカスの問題への対処方法としては、CommandTarget プロパティを通じてコマンド ハンドラを明示的に指定する方法もあります。たとえば、図 5 では有効にできなかったウィンドウ レベルの [Save] (保存) ボタンの場合、そのコマンド フックアップを次のように変更できます。
<Button Command="Save"
CommandTarget="{Binding ElementName=uc1}"
Width="75" Height="25">Save</Button>
このコードでは、Button の CommandTarget は、コマンド ハンドラの含まれる UIElement インスタンスに明示的に設定されています。ここでは uc1 という名前の要素を指定していますが、これは上記の例の 2 つのユーザー コントロール インスタンスの一方に相当します。この要素には常に CanExecute = true を返すコマンド ハンドラがあるため、ウィンドウ レベルの [Save] (保存) ボタンは常に有効になり、コマンド ハンドラを基準とした呼び出し元の位置に関係なく、そのコントロールのコマンド ハンドラだけを呼び出します。
ルーティング コマンドの先を行く
ルーティング コマンドには制限があるため、WPF を使用して複雑な UI の構築に取り組んでいる多くの企業では、カスタム ICommand の実装を利用し始めています。これを利用すると、独自のルーティング メカニズム (特に、ビジュアル ツリーに結び付けられていない、複数のコマンド ハンドラをサポートできるルーティング メカニズム) を実現できます。
カスタム コマンドの実装を作成するのは、難しくありません。ICommand インターフェイスをクラスで実装し、コマンド ハンドラをフックする手段を提供して、コマンドが呼び出されたときにルーティングが実行されるようにします。また、CanExecuteChanged イベントを生成するタイミングを決定するためにどのような基準を使用するかを定める必要があります。
カスタム コマンドを作成する場合は、まずデリゲートを使用するとよいでしょう。デリゲートはターゲット メソッドの呼び出しをサポートしているうえに、複数のサブスクライバもサポートしています。
図 6 に、デリゲートを使用して複数のハンドラのフックを実現する、StringDelegateCommand と呼ばれるカスタム コマンド クラスを示します。このクラスは、ハンドラへの文字列引数の受け渡しをサポートしています。また、呼び出し元の CommandParameter を使用して、ハンドラに渡されるメッセージを特定します。

図 6 カスタム コマンド クラス
public class StringDelegateCommand : ICommand {
Action<string> m_ExecuteTargets = delegate { };
Func<bool> m_CanExecuteTargets = delegate { return false; };
bool m_Enabled = false;
public bool CanExecute(object parameter) {
Delegate[] targets = m_CanExecuteTargets.GetInvocationList();
foreach (Func<bool> target in targets) {
m_Enabled = false;
bool localenable = target.Invoke();
if (localenable) {
m_Enabled = true;
break;
}
}
return m_Enabled;
}
public void Execute(object parameter) {
if (m_Enabled)
m_ExecuteTargets(parameter != null ? parameter.ToString() : null);
}
public event EventHandler CanExecuteChanged = delegate { };
...
}
おわかりのように、Func<bool> デリゲートを使用して、コマンドが有効かどうかを判断するハンドラをフックしています。CanExecute の実装では、クラスは m_CanExecuteTargets デリゲートにフックされているハンドラを通じてループし、実行すべきハンドラがあるかどうかを確認します。実行すべきハンドラがある場合は true を返し、StringDelegateCommand は有効になります。Execute メソッドが呼び出されると、このメソッドは単にコマンドが有効であるかどうかを確認し、コマンドが有効な場合、m_ExecuteTargets Action<string> デリゲートにフックされているすべてのハンドラを呼び出します。
ハンドラを CanExecute メソッドおよび Execute メソッドにフックするため、StringDelegateCommand クラスは図 7 に示すイベント アクセサを公開し、ハンドラが基となるデリゲートをサブスクライブ/アンサブスクライブできるようにします。また、イベント アクセサによって、ハンドラがサブスクライブ/アンサブスクライブした際に CanExecuteChanged イベントをトリガできるようになります。

図 7 コマンドのイベント アクセサ
public event Action<string> ExecuteTargets {
add {
m_ExecuteTargets += value;
}
remove {
m_ExecuteTargets -= value;
}
}
public event Func<bool> CanExecuteTargets {
add {
m_CanExecuteTargets += value;
CanExecuteChanged(this, EventArgs.Empty);
}
remove {
m_CanExecuteTargets -= value;
CanExecuteChanged(this, EventArgs.Empty);
}
}
ルーティング ハンドラのサンプル
コード サンプルのサンプル アプリケーションでは、このクラスをフックしています。サンプルには、背後にプレゼンタのあるシンプルなビューが含まれます (MVP に従っていますが、モデルは含まれていません)。プレゼンタはデータ バインド用のビューにプレゼンテーション モデルを公開します (プレゼンテーション モデルは、プレゼンタとビューの間に位置すると考えることができます。一方、MVP のモデルはビューのプレゼンタの背後に位置します)。プレゼンテーション モデルは、通常、ビューがデータをバインドできるプロパティを公開します。この場合は、コマンドの単一のプロパティだけを公開し、データ バインドを通じてビューの XAML に簡単にフックできるようにします。
<Window x:Class="CustomCommandsDemo.SimpleView" ...>
<Grid>
<Button Command="{Binding CookDinnerCommand}"
CommandParameter="Dinner is served!" ...>Cook Dinner</Button>
<Button Click="OnAddHandler" ...>Add Cook Dinner Handler</Button>
</Grid>
</Window>
Binding ステートメントは現在の DataContext で CookDinnerCommand という名前のプロパティを探し、それが見つかった場合には ICommand へのキャストを試みます。既に説明したように、CommandParameter は、呼び出し元がコマンドと共に一部のデータを渡すための手段です。ここでは、StringDelegateCommand を通じてハンドラに渡される文字列だけを渡しています。
ビューの分離コード (Window クラス) を次に示します。
public partial class SimpleView : Window {
SimpleViewPresenter m_Presenter = new SimpleViewPresenter();
public SimpleView() {
InitializeComponent();
DataContext = m_Presenter.Model;
}
private void OnAddHandler(object sender, RoutedEventArgs e) {
m_Presenter.AddCommandHandler();
}
}
ビューはそのプレゼンタを構成し、プレゼンタからプレゼンテーション モデルを取得します。そして、そのプレゼンテーション モデルを DataContext として設定します。ビューにはボタンの Click ハンドラもあります。このハンドラはプレゼンタを呼び出し、コマンドのハンドラを追加するようにプレゼンタに指示します。
図 8 に、実行中のこのアプリケーションを示します。最初のウィンドウは、コマンド ハンドラがフックされていない初期状態のウィンドウです。コマンド ハンドラがないため、最初のボタン (呼び出し元) が無効になっていることがわかります。2 番目のボタンをクリックすると、プレゼンタが呼び出されて新しいコマンド ハンドラがフックされます。すると、最初のボタンが有効になります。そのボタンをクリックすると、データ バインドと基となるコマンドのサブスクライバ リストを通じてボタンが緩やかに結合されているコマンド ハンドラが呼び出されます。
図 8 実行中のカスタム コマンドのサンプル (クリックすると拡大画像が表示されます)
プレゼンタ コードを図 9 に示します。プレゼンタがプレゼンテーション モデルを構成し、それを Model プロパティを通じてビューに公開していることがわかります。AddCommandHandler が (2 番目のボタンの Click イベントに反応して) ビューから呼び出されると、AddCommandHandler によってモデルの CanExecuteTargets イベントと ExecuteTargets イベントにサブスクライバが追加されます。これらのサブスクリプション メソッドはプレゼンタにある単純なメソッドであり、それぞれ true を返して MessageBox を表示します。

図 9 プレゼンタ クラス
public class SimpleViewPresenter {
public SimpleViewPresenter() {
Model = new SimpleViewPresentationModel();
}
public SimpleViewPresentationModel Model { get; set; }
public void AddCommandHandler() {
Model.CookDinnerCommand.CanExecuteTargets += CanExecuteHandler;
Model.CookDinnerCommand.ExecuteTargets += ExecuteHandler;
}
bool CanExecuteHandler() {
return true;
}
void ExecuteHandler(string msg) {
MessageBox.Show(msg);
}
}
この例は、データ バインド、UI パターン、およびカスタム コマンドを組み合わせて使用することで、コマンドを実装するための独立した簡潔なアプローチが可能になることを示しています。ルーティング コマンドの制限に縛られることもありません。コマンドはバインドを通じて XAML にフックされます。そのため、このアプローチを応用し、XAML だけを使用してビューを定義すること (分離コードは使用しない)、XAML のバインドされたコマンドを使用してプレゼンテーション モデルでアクションをトリガすること、通常は分離コードで行っているアクションをプレゼンテーション モデルから開始することなどが可能です。
ビューを構成し、そのビューにプレゼンテーション モデルを提供するためのコントローラが必要になりますが、分離コードなしで対話型のビューを記述できます。分離コードが不要になると、UI アプリケーションの開発に付き物の、テスト不可能で複雑な密結合コードを分離コード ファイルに追加する手間をかなり省くことができます。これは WPF における初歩的なアプローチにすぎませんが、検討すべきものであることは間違いありません。さまざまな例を見逃さないようにしてください。
Brian Noyes は、IDesign Inc. (
www.idesign.net) のチーフ アーキテクト、Microsoft Regional Director (
www.theregion.com)、および Microsoft MVP を兼任しています。著書に、『Developing Applications with Windows Workflow Foundation』、『Smart Client Deployment with ClickOnce』、および『Data Binding with Windows Forms 2.0』があります。また、世界各地で開かれる業界のカンファレンスで講演も頻繁に行っています。
briannoyes.net にブログを公開しています。