ルーティング イベントの処理済みとしてのマーキング、およびクラス処理 (WPF .NET)

ルーティング イベントをいつ処理済みとしてマークするかに関する絶対的なルールはありませんが、コードで有意な方法でイベントに応答する場合は、イベントを処理済みとしてマークすることを検討してください。 処理済みとしてマークされているルーティング イベントは、そのルートに沿って続行されますが、処理済みのイベントに応答するように構成されているハンドラーのみが呼び出されます。 基本的に、ルーティング イベントを処理済みとしてマークすると、イベント ルートに沿ったリスナーへの可視性が制限されます。

ルーティング イベント ハンドラーは、インスタンス ハンドラーまたはクラス ハンドラーのいずれかにすることができます。 インスタンス ハンドラーでは、オブジェクトまたは XAML 要素のルーティング イベントを処理します。 クラス ハンドラーではクラス レベルでルーティング イベントを処理し、クラスのインスタンスで同じイベントに応答するインスタンス ハンドラーの前に呼び出されます。 ルーティング イベントが処理済みとしてマークされるときは、多くの場合、クラス ハンドラー内でそのようにマークされます。 この記事では、ルーティング イベントを処理済みとしてマークすることの利点と潜在的な危険性、さまざまな種類のルーティング イベントとルーティング イベント ハンドラー、複合コントロールでのイベントの抑制について説明します。

重要

.NET 7 と .NET 6 用のデスクトップ ガイド ドキュメントは作成中です。

前提条件

この記事では、ルーティング イベントの基本的な知識があり、「ルーティング イベントの概要」をお読みになったことを前提としています。 この記事の例について理解するには、Extensible Application Markup Language (XAML) を使い慣れていて、Windows Presentation Foundation (WPF) アプリケーションの記述方法を理解していると役に立ちます。

ルーティング イベントを処理済みとしてマークするタイミング

通常、各ルーティング イベントに有意な応答を提供するハンドラーは、1 つだけにする必要があります。 ルーティング イベント システムを使用して、複数のハンドラーで有意な応答を提供しないようにしてください。 有意な応答を構成するものの定義は主観的であり、アプリケーションによって異なります。 一般的なガイダンス:

  • 有意な応答には、フォーカスの設定、パブリック状態の変更、ビジュアル表現に影響するプロパティの設定、新しいイベントの生成、イベントの完全な処理が含まれます。
  • 有意ではない応答には、視覚的な影響またはプログラムへの影響のないプライベート状態の変更、イベント ログの記録、イベントに応答せずにイベント データを検査することが含まれます。

一部の WPF コントロールでは、処理済みのイベントとしてマークすることで、それ以上の処理を必要としないコンポーネント レベルのイベントが抑制されます。 コントロールによって処理済みとしてマークされたイベントを処理する場合は、「コントロールによるイベント抑制の回避」を参照してください。

イベントを "処理済み" としてマークするには、イベント データの Handled プロパティ値を true に設定します。 その値を false に戻すことはできますが、それが必要になることはまれです。

プレビューおよびバブリング ルーティング イベントのペア

プレビュー およびバブリング ルーティングイベントのペアは、入力イベントに固有です。 いくつかの入力イベントでは、トンネリングおよびバブリング ルーティング イベントのペア (PreviewKeyDownKeyDown など) が実装されます。 Preview プレフィックスは、プレビュー イベントが完了するとバブリング イベントが開始されることを示します。 各プレビューおよびバブリング イベントのペアでは、イベント データの同じインスタンスが共有されます。

ルーティング イベント ハンドラーは、イベントのルーティング戦略に対応する順序で呼び出されます。

  1. プレビュー イベントでは、アプリケーションのルート要素からルーティング イベントを発生させた要素まで移動します。 アプリケーションのルート要素にアタッチされたプレビュー イベント ハンドラーが最初に呼び出され、その後、後続の入れ子になった要素にアタッチされたハンドラーが呼び出されます。
  2. プレビュー イベントの完了後、ペアリングされたバブリング イベントでは、ルーティング イベントを発生させた要素からアプリケーションのルート要素に移動します。 ルーティング イベントを発生させたのと同じ要素にアタッチされたバブリング イベント ハンドラーが最初に呼び出され、その後、後続の親要素にアタッチされたハンドラーが呼び出されます。

ペアリングされたプレビューおよびバブリング イベントは、独自のルーティング イベントを宣言して発生させるいくつかの WPF クラスの内部実装の一部です。 クラス レベルの内部実装がない場合、イベントの名前付けに関係なく、プレビューおよびバブリング ルーティング イベントは完全に分離され、イベント データは共有されません。 カスタム クラスでバブリングまたはトンネリング入力ルーティング イベントを実装する方法の詳細については、カスタム ルーティング イベントの作成に関する記事を参照してください。

プレビュー イベントとバブリング イベントの各ペアではイベント データの同じインスタンスが共有されるため、プレビュー ルーティング イベントが処理済みとしてマークされると、ペアリングされたバブリング イベントも処理済みになります。 バブリング ルーティング イベントが処理済みとしてマークされた場合、プレビュー イベントは完了しているため、ペアリングされたプレビュー イベントには影響しません。 プレビューおよびバブリング入力イベントのペアを処理済みとしてマークする場合は注意してください。 処理済みのプレビュー入力イベントでは、トンネリング ルートの残りの部分に対して通常登録されているイベント ハンドラーは呼び出されず、ペアリングされたバブリング イベントは発生しません。 処理済みのバブリング入力イベントでは、バブリング ルートの残りの部分に対して通常登録されているイベント ハンドラーは呼び出されません。

インスタンスおよびクラス ルーティング イベント ハンドラー

ルーティング イベント ハンドラーは、"インスタンス" ハンドラーまたは "クラス" ハンドラーのいずれかにすることができます。 特定のクラスのクラス ハンドラーは、そのクラスのインスタンスで同じイベントに応答するインスタンス ハンドラーの前に呼び出されます。 この動作により、ルーティング イベントが処理済みとしてマークされるときは、多くの場合、クラス ハンドラー内でそのようにマークされます。 クラス ハンドラーには次の 2 つの種類があります。

インスタンス イベント ハンドラー

インスタンス ハンドラーをオブジェクトまたは XAML 要素にアタッチするには、AddHandler メソッドを直接呼び出します。 WPF ルーティング イベントでは、AddHandler メソッドを使用してイベント ハンドラーをアタッチする共通言語ランタイム (CLR) イベント ラッパーが実装されます。 イベント ハンドラーをアタッチするための XAML 属性構文は CLR イベント ラッパーの呼び出しになるため、XAML でハンドラーをアタッチする場合も AddHandler の呼び出しに解決されます。 処理済みのイベントの場合は、次のようになります。

  • XAML 属性構文または AddHandler の共通シグネチャを使用してアタッチされたハンドラーは呼び出されません。
  • handledEventsToo パラメーターに true を設定して AddHandler(RoutedEvent, Delegate, Boolean) のオーバーロードを使用してアタッチされたハンドラーは呼び出されます。 このオーバーロードは、処理済みのイベントに応答する必要があるまれなケースで使用できます。 たとえば、要素ツリー内の一部の要素ではイベントが処理済みとしてマークされているが、イベント ルートに沿った他の要素ではその処理済みのイベントに応答する必要がある場合です。

次の XAML サンプルでは、componentTextBox という名前の TextBoxouterStackPanel という名前の StackPanel にラップする componentWrapper という名前のカスタム コントロールを追加しています。 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 コンストラクターでは、handledEventsToo パラメーターに true を設定して UIElement.AddHandler(RoutedEvent, Delegate, Boolean) のオーバーロードを使用し、KeyDown バブリング イベントのインスタンス ハンドラーを componentWrapper にアタッチします。 その結果、このインスタンス イベント ハンドラーでは、未処理のイベントと処理済みのイベントの両方に応答するようになります。

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 クラスは、StackPanel クラスから派生した ComponentWrapperBase クラスから派生したものです。 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 イベント システムでは、クラス階層内の最も派生したクラスのオーバーライド クラス イベント ハンドラーのみが呼び出されます。 その後、クラス階層内の最も派生したクラスでは、base キーワードを使用して仮想メソッドの基本実装を呼び出すことができます。 ほとんどの場合、イベントを処理済みとしてマークするかどうかに関係なく、基本実装を呼び出す必要があります。 基本実装の呼び出しを省略するのは、基本実装のロジックを置き換える要件がクラスにある場合だけにしてください。 基本実装をオーバーライドするコードの前と後のどちらで呼び出すかは、実装の特性によって異なります。

前のコード サンプルでは、基底クラス OnKeyDown の仮想メソッドは、ComponentWrapper および ComponentWrapperBase クラスの両方でオーバーライドされます。 WPF イベント システムでは ComponentWrapper.OnKeyDown オーバーライド クラス イベント ハンドラーのみが呼び出されるので、そのハンドラーでは base.OnKeyDown(e) を使用して ComponentWrapperBase.OnKeyDown オーバーライド クラス イベント ハンドラーを呼び出します。さらにそのハンドラーでは base.OnKeyDown(e) を使用して StackPanel.OnKeyDown 仮想メソッドが呼び出されます。 前のコード サンプルのイベントの順序は次のとおりです。

  1. componentWrapper にアタッチされたインスタンス ハンドラーが、PreviewKeyDown ルーティング イベントによってトリガーされます。
  2. componentWrapper にアタッチされた静的クラス ハンドラーが、KeyDown ルーティング イベントによってトリガーされます。
  3. componentWrapperBase にアタッチされた静的クラス ハンドラーが、KeyDown ルーティング イベントによってトリガーされます。
  4. componentWrapper にアタッチされたオーバーライド クラス ハンドラーが、KeyDown ルーティング イベントによってトリガーされます。
  5. componentWrapperBase にアタッチされたオーバーライド クラス ハンドラーが、KeyDown ルーティング イベントによってトリガーされます。
  6. KeyDown ルーティング イベントが処理済みとしてマークされます。
  7. componentWrapper にアタッチされたインスタンス ハンドラーが、KeyDown ルーティング イベントによってトリガーされます。 handledEventsToo パラメーターを true に設定して、ハンドラーが登録されました。

複合コントロールでの入力イベントの抑制

一部の複合コントロールでは、コンポーネント レベルで入力イベントが抑制され、より多くの情報を運ぶか、より具体的な動作を意味する、カスタマイズされた高レベルのイベントにそれらが置き換えられます。 複合コントロールは、その名のとおり、複数の実用的なコントロールまたはコントロールの基底クラスで構成されています。 典型的な例としては、さまざまなマウス イベントを Click ルーティング イベントに変換する Button コントロールがあります。 Button の基底クラスは、UIElement から間接的に派生した ButtonBase です。 コントロールの入力処理に必要なイベント インフラストラクチャの大半は、UIElement のレベルにあります。 UIElement では MouseLeftButtonDownMouseRightButtonDown などのいくつかの 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 を処理済みとしてマークしないように注意してください。

入力イベントの抑制を回避する方法の例については、「コントロールによるイベント抑制の回避」を参照してください。

参照