ルーティング イベントの処理済みとしてのマーキング、およびクラス処理
更新 : 2007 年 11 月
ルーティング イベントのハンドラでは、イベント データ内で、イベントを処理済みとしてマークできます。イベントを処理すると、ルートが事実上短縮されます。クラス処理は、ルーティング イベントでサポートされているプログラミング概念です。クラス ハンドラでは、特定のルーティング イベントをクラス レベルのハンドラで処理することができます。このハンドラは、そのクラスのどのインスタンスのどのインスタンス ハンドラよりも先に呼び出されます。
このトピックには次のセクションが含まれています。
- 必要条件
- イベントを処理済みとしてマークする場合
- "Preview" (トンネル) イベントとバブル イベントのイベント処理
- クラス ハンドラとインスタンス ハンドラ
- コントロールの基本クラスでのルーティング イベントのクラス処理
- イベントが処理済みとしてマークされていても呼び出されるインスタンス ハンドラの追加
- コントロール複合の入力イベントの意図的な抑制
- 関連トピック
必要条件
ここでは、「ルーティング イベントの概要」で紹介した概念を詳しく説明します。
イベントを処理済みとしてマークする場合
ルーティング イベントのイベント データで Handled プロパティの値を true に設定することを、"イベントを処理済みとしてマークする" と言います。アプリケーションの作成者や、既存のルーティング イベントへの応答や新しいルーティング イベントの実装を行うコントロールの作成者が、どのような場合にイベントを処理済みとしてマークするかについては、絶対的な規則はありません。ほとんどにおいて、ルーティング イベントのイベント データで運ばれる "処理済み" の概念は、WPFAPI で公開されているさまざまなルーティング イベントや、カスタム ルーティング イベントに対するアプリケーションの応答のための限定的なプロトコルとして使用する必要があります。また、"処理済み" の問題のもう 1 つの考え方として、ルーティング イベントに対するコードの応答が重要かつ比較的完全な形で行われる場合は、一般にルーティング イベントを処理済みとしてマークする必要があります。通常は、1 つのルーティング イベント発生に対して、異なるハンドラ実装を必要とする複数の重要な応答があることは好ましくありません。複数の応答が必要な場合は、ルーティング イベント システムを使用して転送するのではなく、単一のハンドラ内で一連のアプリケーション ロジックとして必要なコードを実装する必要があります。また、何が "重要" かと考えるかも主観的なもので、アプリケーションやコードに応じて異なります。一般的に、"重要な応答" の例には、フォーカスの設定、パブリックな状態の変更、ビジュアル表現に影響するプロパティの設定、他の新しいイベントの発生などがあります。重要でない応答には、プライベートな状態の変更 (ビジュアル表現への影響やプログラムによる表現を伴わない変更) や、イベントのログへの記録などがあります。イベントの引数を確認して応答しないように選択する場合もこれに含まれます。
ルーティング イベントのこの "処理済み" 状態を使用するための "重要な応答" モデルは、ルーティング イベント システムの動作によって支えられています。XAML や AddHandler の一般的なシグネチャで追加されたハンドラは、イベント データが処理済みとしてマークされているルーティング イベントに対しては呼び出されないからです。それまでのイベント ルートの処理によって、処理済みとしてマークされているルーティング イベントを処理するには、handledEventsToo パラメータ バージョン (AddHandler(RoutedEvent, Delegate, Boolean)) を使用してハンドラを追加する必要があります。
場合によっては、コントロール自体が特定のルーティング イベントを処理済みとしてマークすることもあります。ルーティング イベントが処理済みとマークされた場合、ルーティング イベントへの応答としてコントロールが行ったアクションは、コントロールの実装の一部として重要または完全なもので、そのイベントにはそれ以上の処理は必要ない、と WPF コントロールの作成者が判断したことを表しています。これは通常、イベントのクラス ハンドラを追加するか、基本クラスに存在するクラス ハンドラ仮想メソッドをオーバーライドすることによって行われます。このイベント処理は、必要に応じて回避することもできます。詳細については、このトピックの「コントロールによるイベント抑制の回避」を参照してください。
"Preview" (トンネル) イベントとバブル イベントのイベント処理
プレビュー ルーティング イベントは、要素ツリーのトンネル ルートをたどるイベントです。名前付け規則に含まれる "Preview" は、対応するバブル ルーティング イベントより前にプレビュー (トンネル) ルーティング イベントが発生するという入力イベントの原則を表しています。また、トンネルとバブルのペアを持つ入力ルーティング イベントは、別個の処理ロジックを持ちます。トンネル/プレビュー ルーティング イベントがイベント リスナによって処理済みとしてマークされた場合、バブル ルーティング イベントは処理済みとしてマークされます。これは、バブル ルーティング イベントのリスナがそのイベントを受け取る前であっても変わりません。トンネル ルーティング イベントとバブル ルーティング イベントは、厳密には別個のイベントですが、この動作を実現するために、同じイベント データのインスタンスをあえて共有しています。
このトンネル ルーティング イベントとバブル ルーティング イベントの間の関連は、任意の WPF クラスで宣言されたルーティング イベントの発生方法の内部実装によって実現されます。これは入力ルーティング イベントのペアにも当てはまります。このクラスレベルの実装が存在しなければ、名前付けスキームを共有するトンネル ルーティング イベントとバブル ルーティング イベントの間に関連はありません。つまり、そのような実装がなければ、それらは 2 つのまったく別のルーティング イベントとなり、連続して発生することや、イベント データを共有することはなくなります。
カスタム クラスでトンネル/バブル入力ルーティング イベントのペアを実装する方法の詳細については、「方法 : カスタム ルーティング イベントを作成する」を参照してください。
クラス ハンドラとインスタンス ハンドラ
ルーティング イベントでは、クラス リスナとインスタンス リスナという 2 種類のイベント リスナが考慮されます。クラス リスナが存在するのは、型の静的コンストラクタで特殊な EventManager API の RegisterClassHandler が呼び出されたか、要素の基本クラスのクラス ハンドラ仮想メソッドがオーバーライドされた場合です。インスタンス リスナは特定のクラス インスタンス/要素で、AddHandler の呼び出しによって 1 つ以上のハンドラがルーティング イベントにアタッチされています。既存の WPF ルーティング イベントは、そのイベントの共通言語ランタイム (CLR) イベント ラッパーの add{} と remove{} の実装の一部として、AddHandler を呼び出します。属性構文によってイベント ハンドラをアタッチする単純な XAML のメカニズムも、この方法で実現されています。したがって、単純な XAML の使用方法も、究極的には AddHandler の呼び出しと同じことになります。
登録されたハンドラ実装があるかどうか、ビジュアル ツリー内の要素がチェックされます。ハンドラはルート全体で呼び出される可能性があり、その順序は、ルーティング イベントのルーティング方法によってあらかじめ決まっています。たとえばバブル ルーティング イベントでは、イベントを発生させた要素と同じ要素にアタッチされているハンドラが最初に呼び出されます。その後、ルーティング イベントは次の親要素に "浮上して" 到達します。アプリケーションのルート要素に到達するまでこれが繰り返されます。
バブル ルートのルート要素の観点から見ると、イベント引数を処理済みとしてマークするハンドラが、クラス処理や、よりルーティング イベント ソースに近い要素によって呼び出された場合、ルート要素のハンドラは呼び出されません。これにより、イベント ルートは、ルート要素に到達する前に事実上短縮されます。ただし、イベント ルートが完全に停止するわけではありません。クラス ハンドラやインスタンス ハンドラによってルーティング イベントが処理済みとしてマークされた場合にも呼び出されるように、特別な条件を使用してハンドラが追加されている可能性があるためです。詳細については、このトピックの「イベントが処理済みとしてマークされていても呼び出されるインスタンス ハンドラの追加」を参照してください。
イベント ルートより深いレベルでは、クラスのインスタンスに対して、複数のクラス ハンドラが作用している可能性があります。なぜなら、ルーティング イベントのクラス処理モデルでは、クラスの階層構造に属するすべてのクラスが、各ルーティング イベントに対して独自のクラス ハンドラをそれぞれ登録できるためです。各クラス ハンドラは内部ストアに追加され、アプリケーションのイベント ルートが構築されたときには、すべてのクラス ハンドラがイベント ルートに追加されます。クラス ハンドラは、最派生クラスのクラス ハンドラが最初に呼び出され、以下それぞれの基本クラスのクラス ハンドラが順に呼び出されていくように、ルートに追加されます。一般に、クラス ハンドラは、処理済みとしてマークされているルーティング イベントにも反応するようには登録されません。したがって、このクラス処理のしくみでは、次の 2 つの方法のいずれかが考えられます。
派生クラスで、ルーティング イベントを処理済みとしてマークしないハンドラを追加することで、基本クラスから継承されたクラス処理を補完できます。派生クラス ハンドラの後のいずれかの時点で、基本クラス ハンドラが呼び出されるためです。
派生クラスで、ルーティング イベントを処理済みとしてマークするクラス ハンドラを追加することで、基本クラスのクラス処理を置き換えることができます。この方法を使用する場合には注意が必要です。外観、状態のロジック、入力処理、コマンド処理などの部分で、基本コントロールが意図したデザインと変わってしまう可能性があります。
コントロールの基本クラスでのルーティング イベントのクラス処理
イベント ルートの各要素ノードでは、その要素のどのインスタンス リスナよりも先にクラス リスナに、ルーティング イベントに応答する機会が与えられます。このため、特定のコントロール クラス実装ではそれ以上伝播しないルーティング イベントを抑制したり、ルーティング イベントに対してそのクラスの機能である特別な処理を提供したりするために、クラス ハンドラが使用されることもあります。たとえば、特定のクラスのコンテキストでユーザー入力の状態が持つ意味についてより具体的な情報を含む、クラス固有の独自のイベントを発生させることができます。この場合、そのクラス実装によって、より一般的なルーティング イベントが処理済みとしてマークされます。通常、クラス ハンドラは、共有されているイベント データが既に処理済みとしてマークされているルーティング イベントに対しては呼び出されないように追加されます。ただし、例外的なケースのために、RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) には、ルーティング イベントが処理済みとしてマークされている場合にも呼び出されるようにクラス ハンドラを登録するシグネチャもあります。
クラス ハンドラ仮想メソッド
一部の要素 (特に、UIElement などの基本要素) では、その一連のパブリック ルーティング イベントに対応する空の "On*Event" および "OnPreview*Event" の各仮想メソッドが公開されています。これらの仮想メソッドをオーバーライドして、そのルーティング イベントに対するクラス ハンドラを実装することができます。基本要素クラスでは、上で説明したように RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) を使用して、これらの仮想メソッドが、各ルーティング イベントのクラス ハンドラとして登録されています。On*Event 仮想メソッドを使用すると、関連するルーティング イベントのクラス処理の実装が大幅に簡略化され、それぞれの型の静的コンストラクタで特別な初期化を行う必要がなくなります。たとえば、OnDragEnter 仮想メソッドをオーバーライドすることによって、任意の UIElement 派生クラスで DragEnter イベントのクラス処理を追加することができます。オーバーライドの中では、ルーティング イベントを処理する、他のイベントを発生させる、インスタンスの要素プロパティを変更する可能性があるクラス固有のロジックを開始するなどのアクションや、それらのアクションの組み合わせを、自由に選択できます。一般に、こうしたオーバーライドでは、イベントを処理済みとしてマークする場合でも基本実装を呼び出す必要があります。基本実装を呼び出すことが強く推奨されるのは、これらの仮想メソッドは基本クラスで定義されているためです。プロテクト仮想メソッドの標準的な呼び出しパターンでは、それぞれの仮想メソッドから、基本実装を呼び出す形になります。これは、ルーティング イベントのクラス処理の固有のしくみ (任意のインスタンスに対して、最派生クラスのハンドラから基本クラスのハンドラへという順で、クラス階層構造のすべてのクラスのクラス ハンドラが呼び出される) を実質的に置き換え、同様の処理を実現することになります。基本実装を呼び出さないようにするのは、基本クラスの処理ロジックを意図的に変更する必要がある場合だけです。基本実装をオーバーライド コードの前と後のどちらに呼び出すかは、それぞれの実装の性質によって異なります。
入力イベントのクラス処理
すべてのクラス ハンドラ仮想メソッドは、処理済みとしてマークされている共有イベント データがない場合にのみ呼び出されるように登録されます。また、入力イベントを一意にするために、トンネル バージョンとバブル バージョンは一般に連続して発生し、イベント データを共有します。このため、一方がトンネル バージョンでもう一方がバブル バージョンの入力イベントのクラス ハンドラのペアに対しては、イベントがすぐに処理済みとしてマークされないようにする必要があります。イベントを処理済みとしてマークするようにトンネル クラス処理仮想メソッドを実装すると、バブル クラス ハンドラが呼び出されなくなります (トンネル イベントやバブル イベントに対して通常の方法で登録されたインスタンス ハンドラも呼び出されません)。
ノードのクラス処理が完了すると、インスタンス リスナが考慮されます。
イベントが処理済みとしてマークされていても呼び出されるインスタンス ハンドラの追加
AddHandler メソッドには、既に他のハンドラによってイベント データが調整され、イベントが処理済みとしてマークされていても、イベントがルートでその処理要素に到達するたびに必ずイベント システムによって呼び出されるハンドラを追加できる、特殊なオーバーロードがあります。この方法は通常は使用されません。一般にハンドラは、イベントが要素ツリーのどこで処理されるかに関係なく、そのイベントによって影響を受ける可能性があるアプリケーション コードのすべての領域を調整するように作成することができます。これは、複数の結果が求められる場合でも同じです。また、通常は、そのイベントに応答する必要がある要素は 1 つだけなので、適切なアプリケーション ロジックが既に発生していることになります。しかし、イベントが要素ツリーやコントロール複合の他の要素によって既に処理済みとしてマークされていても、要素ツリーのもっと上 (ルートによってはもっと下) にある他の要素のハンドラを呼び出す必要がある場合もあります。そうした例外的なケースに対しては、handledEventsToo オーバーロードを使用することができます。
処理済みのイベントを未処理としてマークする場合
一般には、処理済みとしてマークされているルーティング イベントを未処理としてマークする (Handled の設定を false に戻す) ことは推奨されません。これは、handledEventsToo で動作するハンドラの場合も変わりません。しかし、一部の入力イベントでは、高レベルのイベント表現と低レベルのイベント表現が重複することがあります。ツリー内のある位置では高レベルのイベントが取得され、別の位置では低レベルのイベントが取得される場合です。たとえば、子要素が高レベルのキー イベント (TextInput など) をリッスンし、親要素が低レベルのイベント (KeyDown など) をリッスンしているとします。親要素が低レベルのイベントを処理した場合、直感的には子要素が先にイベントを処理するはずであるにもかかわらず、高レベルのイベントが抑制されてしまうことがあります。
このような状況では、親要素と子要素の両方に低レベルのイベントのハンドラを追加する必要があります。この場合、子要素のハンドラ実装によって低レベルのイベントが処理済みとしてマークされる可能性がありますが、親要素のハンドラ実装がそれを再び未処理に設定して、ツリーのもっと上にある要素 (および高レベルのイベント) がそのイベントに応答できるようにします。この状況は通常はあまりありません。
コントロール複合の入力イベントの意図的な抑制
ルーティング イベントのクラス処理は、主に入力イベントと複合コントロールに対して使用されます。複合コントロールは、その名のとおり、複数の実際的なコントロールまたはコントロールの基本クラスで構成されています。コントロールを作成する際に、それらの各サブコンポーネントで発生するすべての入力イベントを 1 つにまとめて、コントロール全体が 1 つのイベント ソースとして報告されるようにする場合があります。また、コンポーネントからのイベントを完全に抑制する場合や、コンポーネントで定義された別のイベント (より多くの情報を含むイベントや、より具体的な動作を表すイベント) に置き換える場合もあります。ここでは、コンポーネント作成者ならだれもが目にする典型的な例として、すべてのボタンに含まれる直感的なイベントである Click イベントに最終的に対応付けられるマウス イベントが Windows Presentation Foundation (WPF) の Button によって処理されるしくみを見てみます。
Button 基本クラス (ButtonBase) は、Control から派生します。Control はさらに、FrameworkElement と UIElement から派生します。コントロールの入力の処理に必要なイベント インフラストラクチャの大半は、UIElement のレベルにあります。具体的に言うと、UIElement は、その境界内でマウス カーソルのヒット テストを処理する一般的な Mouse イベントを処理し、ほとんどの一般的なボタン アクション (MouseLeftButtonDown など) のための個々のイベントを提供します。また、MouseLeftButtonDown の登録済みクラス ハンドラとして、空の仮想メソッド OnMouseLeftButtonDown を提供します。これを ButtonBase がオーバーライドします。同様に、ButtonBase は MouseLeftButtonUp のクラス ハンドラを使用します。このオーバーライドでは、イベント データが渡されますが、その RoutedEventArgs インスタンスを処理済みとしてマークします (Handled を true に設定します)。その同じイベント データが残りのルートで使用され、他のクラス ハンドラや、インスタンス ハンドラやイベント setter に渡されます。また、OnMouseLeftButtonUp のオーバーライドは、その後 Click イベントを発生させます。その結果、ほとんどのリスナにとっては、MouseLeftButtonDown イベントおよび MouseLeftButtonUp イベントが "消滅" し、Click に置き換えられたことになります。このイベントは、ボタンの複合の一部やまったく別の要素からではなく本当のボタンから発生したものとして認識されるため、より多くの意味を持つと言えます。
コントロールによるイベント抑制の回避
個々のコントロール内で行われるこのイベントの抑制の動作が、アプリケーションのイベント処理ロジックの全体的な目的の妨げになることがあります。たとえば、なんらかの理由でアプリケーションのルート要素に MouseLeftButtonDown のハンドラが配置されていた場合、ボタンをマウスでクリックしてもルート レベルの MouseLeftButtonDown ハンドラや MouseLeftButtonUp ハンドラは呼び出されなくなります。イベント自体は実際に "浮上" します (既に説明したように、処理済みとしてマークされた後、イベント ルートは終了するのではなく、ルーティング イベント システムによってハンドラ呼び出しの動作が変更されます)。ルーティング イベントがボタンに到達すると、より多くの意味を持つ Click イベントに置き換えるために、ButtonBase のクラス処理によって MouseLeftButtonDown が処理済みとしてマークされます。その結果、ルートのさらに上にある標準の MouseLeftButtonDown ハンドラは呼び出されなくなります。このような状況でもハンドラが呼び出されるようにするには 2 つの方法があります。
1 つ目は、AddHandler(RoutedEvent, Delegate, Boolean) の handledEventsToo シグネチャを使用して意図的にハンドラを追加する方法です。この方法には、イベント ハンドラの追加をコードからしか行えず、マークアップからは行うことができないという制限があります。Extensible Application Markup Language (XAML) でイベント属性の値としてイベント ハンドラ名を指定する単純な構文では、この動作は実現できません。
2 つ目の方法は、トンネル バージョンとバブル バージョンのルーティング イベントがペアになっている入力イベントでのみ使用できます。これらのルーティング イベントでは、対応するプレビュー/トンネル ルーティング イベントに、ハンドラを追加することができます。そのルーティング イベントはルートからトンネリングを開始するため、アプリケーション要素ツリーの先祖要素のレベルでプレビュー ハンドラがアタッチされていれば、イベントがボタン クラス処理コードによって遮断されなくなります。この方法を使用する場合は、プレビュー イベントを処理済みとしてマークする際に注意が必要です。PreviewMouseLeftButtonDown をルート要素で処理する例で言うと、ハンドラ実装でイベントを Handled としてマークすると、実際には Click イベントが抑制されます。通常これは望ましくない動作です。