ルーティング イベントを処理済みとクラス処理としてマークする (WPF .NET)
ルーティング イベントをいつ処理済みとしてマークするかに関する絶対的な規則はありませんが、コードがイベントに重要な方法で応答する場合は、イベントを処理済みとしてマークすることを検討してください。 処理済みとしてマークされたルーティング イベントは、そのルートに沿って続行されますが、処理されたイベントに応答するように構成されたハンドラーのみが呼び出されます。 基本的に、ルーティング イベントを処理済みとしてマークすると、イベント ルートに沿ったリスナーへの可視性が制限されます。
ルーティング イベント ハンドラーには、インスタンス ハンドラーまたはクラス ハンドラーのいずれかを指定できます。 インスタンス ハンドラーは、オブジェクトまたは XAML 要素のルーティング イベントを処理します。 クラス ハンドラーは、クラス レベルでルーティング イベントを処理し、クラスの任意のインスタンスで同じイベントに応答するインスタンス ハンドラーの前に呼び出されます。 ルーティング イベントが処理済みとしてマークされると、多くの場合、クラス ハンドラー内でそのようにマークされます。 この記事では、ルーティング イベントを処理済みとしてマークする利点と潜在的な落とし穴、ルーティング イベントとルーティング イベント ハンドラーの種類、複合コントロールでのイベント抑制について説明します。
前提 条件
この記事では、ルーティング イベントに関する基本的な知識と、ルーティング イベントの概要
ルーティング イベントを処理済みとしてマークするタイミング
通常、ルーティングされたイベントごとに 1 つのハンドラーのみが重要な応答を提供する必要があります。 ルーティング イベント システムを使用して、複数のハンドラー間で重要な応答を提供しないようにします。 重要な応答を構成するものの定義は主観的であり、アプリケーションによって異なります。 一般的なガイダンス:
- 重要な応答には、フォーカスの設定、パブリック状態の変更、ビジュアル表現に影響するプロパティの設定、新しいイベントの発生、イベントの完全な処理が含まれます。
- 重要でない応答には、ビジュアルやプログラムに影響を与えずにプライベート状態を変更する、イベント ログを記録する、イベントに応答せずにイベント データを調べるなどがあります。
一部の WPF コントロールでは、それ以上処理する必要のないコンポーネント レベルのイベントを、処理されたイベントとしてマークすることで抑制されます。 コントロールによって処理済みとしてマークされたイベントを処理する場合は、「コントロールによるイベント抑制を回避する」を参照してください。
イベントを としてマークするには、イベントデータの Handled プロパティ値を true
に設定します。 その値を false
に戻ることは可能ですが、そうする必要性はまれです。
ルーティング イベント ペアのプレビューと伝播
プレビュー とバブル ルーティング イベントのペアは、入力イベントに特有です。 いくつかの入力イベントは、PreviewKeyDown や KeyDownなどの トンネリング と バブル ルーティング イベント のペアを実装します。 Preview
プレフィックスは、プレビュー イベントが完了するとバブル イベントが開始されることを示します。 プレビュー イベントとバブル イベントの各ペアは、イベント データの同じインスタンスを共有します。
ルーティング イベント ハンドラーは、イベントのルーティング戦略に対応する順序で呼び出されます。
- プレビュー イベントは、アプリケーションのルート要素から、ルーティング イベントを発生させた要素まで移動します。 アプリケーション ルート要素にアタッチされたプレビュー イベント ハンドラーが最初に呼び出され、続いて連続する入れ子になった要素にアタッチされたハンドラーが呼び出されます。
- プレビュー イベントが完了すると、ペアのバブル イベントは、ルーティング イベントを発生させた要素からアプリケーションルート要素に移動します。 ルーティング イベントを発生させたのと同じ要素にアタッチされたバブル イベント ハンドラーが最初に呼び出され、続いて連続する親要素にアタッチされたハンドラーが呼び出されます。
ペアのプレビュー イベントとバブル イベントは、独自のルーティング イベントを宣言して発生させるいくつかの WPF クラスの内部実装の一部です。 クラス レベルの内部実装がないと、プレビュー イベントとバブル ルーティング イベントは完全に分離され、イベントの名前付けに関係なく、イベント データは共有されません。 カスタム クラスでバブルまたはトンネリング入力ルーティング イベントを実装する方法については、「カスタム ルーティング イベントを作成する」を参照してください。
各プレビュー イベントとバブル イベント ペアはイベント データの同じインスタンスを共有するため、プレビュー ルーティング イベントが処理済みとしてマークされている場合は、ペアのバブル イベントも処理されます。 バブル ルーティング イベントが処理済みとしてマークされている場合、プレビュー イベントが完了したため、ペアのプレビュー イベントには影響しません。 プレビューとバブル入力イベントのペアを処理済みとしてマークするときは注意してください。 処理されたプレビュー入力イベントは、トンネリング ルートの残りの部分に対して通常登録されているイベント ハンドラーを呼び出しません。ペアのバブル イベントは発生しません。 処理されたバブル入力イベントは、バブル ルートの残りの部分に対して通常登録されているイベント ハンドラーを呼び出しません。
インスタンスとクラスルーティング イベント ハンドラー
ルーティング イベント ハンドラーは、インスタンス ハンドラーまたはクラス ハンドラーのいずれかです。 特定のクラスのクラス ハンドラーは、そのクラスの任意のインスタンスで同じイベントに応答するインスタンス ハンドラーの前に呼び出されます。 この動作により、ルーティング イベントが処理済みとしてマークされると、多くの場合、クラス ハンドラー内でそのようにマークされます。 クラス ハンドラーには、次の 2 種類があります。
- 静的クラス イベント ハンドラー。静的クラス コンストラクター内で RegisterClassHandler メソッドを呼び出すことによって登録されます。
- 基底クラスの仮想イベント メソッドをオーバーライドすることによって登録されるクラス イベント ハンドラーをオーバーライドします。 基本クラスの仮想イベント メソッドは主に入力イベント用に存在し、
On で始まり、OnPreviewイベント名 イベント名 名前を持ちます。
インスタンス イベント ハンドラー
AddHandler メソッドを直接呼び出すことで、インスタンス ハンドラーをオブジェクトまたは XAML 要素にアタッチできます。 WPF ルーティング イベントは、AddHandler
メソッドを使用してイベント ハンドラーをアタッチする共通言語ランタイム (CLR) イベント ラッパーを実装します。 イベント ハンドラーをアタッチするための XAML 属性構文では CLR イベント ラッパーが呼び出されるため、XAML でハンドラーをアタッチしても AddHandler
呼び出しに解決されます。 処理されたイベントの場合:
- XAML 属性構文または
AddHandler
の共通シグネチャを使用してアタッチされたハンドラーは呼び出されません。 - AddHandler(RoutedEvent, Delegate, Boolean) オーバーロードを使用して
handledEventsToo
パラメーターがtrue
に設定され、アタッチされたハンドラーが呼び出されます。 このオーバーロードは、処理されたイベントに応答する必要があるまれなケースで使用できます。 たとえば、要素ツリー内の一部の要素はイベントを処理済みとしてマークしていますが、イベント ルートに沿った他の要素は、処理されたイベントに応答する必要があります。
次の XAML サンプルでは、componentWrapper
という名前のカスタム コントロールを追加します。このコントロールは、componentTextBox
という名前の TextBox を、outerStackPanel
という名前の StackPanel にラップします。 PreviewKeyDown イベントのインスタンス イベント ハンドラーは、XAML 属性構文を使用して componentWrapper
にアタッチされます。 その結果、インスタンス ハンドラーは、componentTextBox
によって発生したハンドルされない PreviewKeyDown
トンネリング イベントにのみ応答します。
<StackPanel Name="outerStackPanel" VerticalAlignment="Center">
<custom:ComponentWrapper
x:Name="componentWrapper"
TextBox.PreviewKeyDown="HandlerInstanceEventInfo"
HorizontalAlignment="Center">
<TextBox Name="componentTextBox" Width="200" />
</custom:ComponentWrapper>
</StackPanel>
MainWindow
コンストラクターは、UIElement.AddHandler(RoutedEvent, Delegate, Boolean) オーバーロードを使用して、KeyDown
バブル イベントのインスタンス ハンドラーを componentWrapper
にアタッチし、handledEventsToo
パラメーターを true
に設定します。 その結果、インスタンス イベント ハンドラーは、未処理のイベントと処理されたイベントの両方に応答します。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Attach an instance handler on componentWrapper that will be invoked by handled KeyDown events.
componentWrapper.AddHandler(KeyDownEvent, new RoutedEventHandler(Handler.InstanceEventInfo),
handledEventsToo: true);
}
// The handler attached to componentWrapper in XAML.
public void HandlerInstanceEventInfo(object sender, KeyEventArgs e) =>
Handler.InstanceEventInfo(sender, e);
}
Partial Public Class MainWindow
Inherits Window
Public Sub New()
InitializeComponent()
' Attach an instance handler on componentWrapper that will be invoked by handled KeyDown events.
componentWrapper.[AddHandler](KeyDownEvent, New RoutedEventHandler(AddressOf InstanceEventInfo),
handledEventsToo:=True)
End Sub
' The handler attached to componentWrapper in XAML.
Public Sub HandlerInstanceEventInfo(sender As Object, e As KeyEventArgs)
InstanceEventInfo(sender, e)
End Sub
End Class
次のセクションでは、ComponentWrapper
のコードビハインドの実装を示します。
静的クラス イベント ハンドラー
クラスの静的コンストラクターで RegisterClassHandler メソッドを呼び出すことで、静的クラス イベント ハンドラーをアタッチできます。 クラス階層内の各クラスは、ルーティング イベントごとに独自の静的クラス ハンドラーを登録できます。 その結果、イベント ルート内の任意のノードで、同じイベントに対して複数の静的クラス ハンドラーが呼び出される可能性があります。 イベントのイベント ルートが構築されると、各ノードのすべての静的クラス ハンドラーがイベント ルートに追加されます。 ノードでの静的クラス ハンドラーの呼び出しの順序は、最も派生した静的クラス ハンドラーから始まり、その後に連続する各基底クラスの静的クラス ハンドラーが続きます。
handledEventsToo
パラメーターが true
に設定された RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) オーバーロードを使用して登録された静的クラス イベント ハンドラーは、未処理のルーティング イベントと処理されたルーティング イベントの両方に応答します。
静的クラス ハンドラーは通常、未処理のイベントにのみ応答するように登録されます。 その場合、ノード上の派生クラス ハンドラーがイベントを処理済みとしてマークした場合、そのイベントの基底クラス ハンドラーは呼び出されません。 このシナリオでは、基底クラス ハンドラーは実質的に派生クラス ハンドラーに置き換えられます。 基底クラス ハンドラーは、多くの場合、外観、状態ロジック、入力処理、コマンド処理などの領域でのデザインの制御に役立ちます。そのため、置き換えることに注意してください。 イベントを処理済みとしてマークしない派生クラス ハンドラーは、基底クラス ハンドラーを置き換える代わりに補足します。
次のコード サンプルは、前の XAML で参照された ComponentWrapper
カスタム コントロールのクラス階層を示しています。 ComponentWrapper
クラスは、ComponentWrapperBase
クラスから派生し、StackPanel クラスから派生します。 RegisterClassHandler
メソッドは、ComponentWrapper
クラスと ComponentWrapperBase
クラスの静的コンストラクターで使用され、これらの各クラスの静的クラス イベント ハンドラーを登録します。 WPF イベント システムは、ComponentWrapperBase
静的クラス ハンドラーの前に ComponentWrapper
静的クラス ハンドラーを呼び出します。
public class ComponentWrapper : ComponentWrapperBase
{
static ComponentWrapper()
{
// Class event handler implemented in the static constructor.
EventManager.RegisterClassHandler(typeof(ComponentWrapper), KeyDownEvent,
new RoutedEventHandler(Handler.ClassEventInfo_Static));
}
// Class event handler that overrides a base class virtual method.
protected override void OnKeyDown(KeyEventArgs e)
{
Handler.ClassEventInfo_Override(this, e);
// Call the base OnKeyDown implementation on ComponentWrapperBase.
base.OnKeyDown(e);
}
}
public class ComponentWrapperBase : StackPanel
{
// Class event handler implemented in the static constructor.
static ComponentWrapperBase()
{
EventManager.RegisterClassHandler(typeof(ComponentWrapperBase), KeyDownEvent,
new RoutedEventHandler(Handler.ClassEventInfoBase_Static));
}
// Class event handler that overrides a base class virtual method.
protected override void OnKeyDown(KeyEventArgs e)
{
Handler.ClassEventInfoBase_Override(this, e);
e.Handled = true;
Debug.WriteLine("The KeyDown routed event is marked as handled.");
// Call the base OnKeyDown implementation on StackPanel.
base.OnKeyDown(e);
}
}
Public Class ComponentWrapper
Inherits ComponentWrapperBase
Shared Sub New()
' Class event handler implemented in the static constructor.
EventManager.RegisterClassHandler(GetType(ComponentWrapper), KeyDownEvent,
New RoutedEventHandler(AddressOf ClassEventInfo_Static))
End Sub
' Class event handler that overrides a base class virtual method.
Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
ClassEventInfo_Override(Me, e)
' Call the base OnKeyDown implementation on ComponentWrapperBase.
MyBase.OnKeyDown(e)
End Sub
End Class
Public Class ComponentWrapperBase
Inherits StackPanel
Shared Sub New()
' Class event handler implemented in the static constructor.
EventManager.RegisterClassHandler(GetType(ComponentWrapperBase), KeyDownEvent,
New RoutedEventHandler(AddressOf ClassEventInfoBase_Static))
End Sub
' Class event handler that overrides a base class virtual method.
Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
ClassEventInfoBase_Override(Me, e)
e.Handled = True
Debug.WriteLine("The KeyDown event is marked as handled.")
' Call the base OnKeyDown implementation on StackPanel.
MyBase.OnKeyDown(e)
End Sub
End Class
このコード サンプルのオーバーライド クラス イベント ハンドラーの分離コード実装については、次のセクションで説明します。
クラス イベント ハンドラーをオーバーライドする
一部のビジュアル要素の基底クラスでは、空の On<イベント名> を公開し、OnPreview<イベント名> パブリック ルーティング入力イベントごとに仮想メソッドを使用します。 たとえば、UIElement は、OnKeyDown と OnPreviewKeyDown の仮想イベント ハンドラーなどを実装します。 基底クラスの仮想イベント ハンドラーをオーバーライドして、派生クラスのオーバーライド クラス イベント ハンドラーを実装できます。 たとえば、OnDragEnter 仮想メソッドをオーバーライドすることで、任意の UIElement
派生クラスに DragEnter イベントのオーバーライド クラス ハンドラーを追加できます。 基底クラスの仮想メソッドのオーバーライドは、クラス ハンドラーを静的コンストラクターに登録するよりも簡単にクラス ハンドラーを実装する方法です。 オーバーライド内では、イベントを発生させたり、クラス固有のロジックを開始してインスタンスの要素プロパティを変更したり、イベントを処理済みとしてマークしたり、その他のイベント処理ロジックを実行したりできます。
静的クラス イベント ハンドラーとは異なり、WPF イベント システムは、クラス階層内の最も派生クラスのオーバーライド クラス イベント ハンドラーのみを呼び出します。 クラス階層内で最も派生したクラスは、基本 キーワードを使用して、仮想メソッドの基本実装を呼び出すことができます。 ほとんどの場合、イベントを処理済みとしてマークするかどうかに関係なく、基本実装を呼び出す必要があります。 基底実装ロジックを置き換える必要があるクラスがある場合にのみ、基本実装の呼び出しを省略する必要があります。 オーバーライドするコードの前または後に基本実装を呼び出すかどうかは、実装の性質によって異なります。
前のコード サンプルでは、基底クラス OnKeyDown
仮想メソッドは、ComponentWrapper
クラスと ComponentWrapperBase
クラスの両方でオーバーライドされます。 WPF イベント システムは ComponentWrapper.OnKeyDown
オーバーライド クラス イベント ハンドラーのみを呼び出すので、そのハンドラーは base.OnKeyDown(e)
を使用してクラス イベント ハンドラーをオーバーライド ComponentWrapperBase.OnKeyDown
呼び出し、base.OnKeyDown(e)
を使用して StackPanel.OnKeyDown
仮想メソッドを呼び出します。 前のコード サンプルのイベントの順序は次のとおりです。
componentWrapper
にアタッチされたインスタンス ハンドラーは、PreviewKeyDown
ルーティング イベントによってトリガーされます。componentWrapper
にアタッチされた静的クラス ハンドラーは、KeyDown
ルーティング イベントによってトリガーされます。componentWrapperBase
にアタッチされた静的クラス ハンドラーは、KeyDown
ルーティング イベントによってトリガーされます。componentWrapper
にアタッチされたオーバーライド クラス ハンドラーは、KeyDown
ルーティング イベントによってトリガーされます。componentWrapperBase
にアタッチされたオーバーライド クラス ハンドラーは、KeyDown
ルーティング イベントによってトリガーされます。KeyDown
ルーティング イベントは処理済みとしてマークされています。componentWrapper
にアタッチされたインスタンス ハンドラーは、KeyDown
ルーティング イベントによってトリガーされます。 ハンドラーは、handledEventsToo
パラメーターをtrue
に設定して登録されました。
複合コントロールでの入力イベント抑制
一部の複合コントロールでは、コンポーネント レベルで Button
基底クラスは ButtonBaseされ、UIElementから間接的に派生します。 制御入力処理に必要なイベント インフラストラクチャの多くは、UIElement
レベルで利用できます。 UIElement
は、MouseLeftButtonDown や MouseRightButtonDownなど、いくつかの Mouse イベントを公開します。 UIElement
は、事前登録されたクラス ハンドラーとして OnMouseLeftButtonDown および OnMouseRightButtonDown 空の仮想メソッドも実装します。 ButtonBase
は、これらのクラス ハンドラーをオーバーライドし、オーバーライド ハンドラー内で Handled プロパティを true
に設定し、Click
イベントを発生させます。 ほとんどのリスナーの最終的な結果は、MouseLeftButtonDown
イベントと MouseRightButtonDown
イベントが非表示になり、高レベルの Click
イベントが表示されます。
入力イベント抑制の対策
個々のコントロール内のイベント抑制が、アプリケーションのイベント処理ロジックに干渉する場合があります。 たとえば、アプリケーションで XAML 属性構文を使用して XAML ルート要素の MouseLeftButtonDown イベントのハンドラーをアタッチした場合、Button コントロールによって MouseLeftButtonDown
イベントが処理済みとしてマークされるため、そのハンドラーは呼び出されません。 処理されたルーティング イベントに対してアプリケーションのルートに向かって要素を呼び出す場合は、次のいずれかを実行できます。
handledEventsToo
パラメーターをtrue
に設定して、UIElement.AddHandler(RoutedEvent, Delegate, Boolean) メソッドを呼び出してハンドラーをアタッチします。 この方法では、アタッチ先の要素のオブジェクト参照を取得した後、バックエンドコードでイベントハンドラーをアタッチする必要があります。処理済みとしてマークされたイベントがバブル入力イベントである場合は、ペアのプレビュー イベントのハンドラーをアタッチします (使用可能な場合)。 たとえば、コントロールが
MouseLeftButtonDown
イベントを抑制する場合は、代わりに PreviewMouseLeftButtonDown イベントのハンドラーをアタッチできます。 この方法は、イベント データを共有するプレビューとバブル入力イベントのペアでのみ機能します。 Click イベントが完全に抑制されるため、PreviewMouseLeftButtonDown
を処理済みとしてマークしないように注意してください。
入力イベント抑制を回避する方法の例については、「コントロールによるイベント抑制の回避を参照してください。
参考
.NET Desktop feedback