次の方法で共有


ルーティング イベントを処理済みとクラス処理としてマークする (WPF .NET)

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

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

前提 条件

この記事では、ルーティング イベントに関する基本的な知識と、ルーティング イベントの概要読んだことを前提としています。 この記事の例に従うと、拡張アプリケーション マークアップ言語 (XAML) に慣れている場合や、Windows Presentation Foundation (WPF) アプリケーションを記述する方法を理解している場合に役立ちます。

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

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

  • 重要な応答には、フォーカスの設定、パブリック状態の変更、ビジュアル表現に影響するプロパティの設定、新しいイベントの発生、イベントの完全な処理が含まれます。
  • 重要でない応答には、ビジュアルやプログラムに影響を与えずにプライベート状態を変更する、イベント ログを記録する、イベントに応答せずにイベント データを調べるなどがあります。

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

イベントを としてマークするには、イベントデータの Handled プロパティ値を trueに設定します。 その値を falseに戻ることは可能ですが、そうする必要性はまれです。

ルーティング イベント ペアのプレビューと伝播

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

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

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

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

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

インスタンスとクラスルーティング イベント ハンドラー

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

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

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 は、OnKeyDownOnPreviewKeyDown の仮想イベント ハンドラーなどを実装します。 基底クラスの仮想イベント ハンドラーをオーバーライドして、派生クラスのオーバーライド クラス イベント ハンドラーを実装できます。 たとえば、OnDragEnter 仮想メソッドをオーバーライドすることで、任意の UIElement 派生クラスに DragEnter イベントのオーバーライド クラス ハンドラーを追加できます。 基底クラスの仮想メソッドのオーバーライドは、クラス ハンドラーを静的コンストラクターに登録するよりも簡単にクラス ハンドラーを実装する方法です。 オーバーライド内では、イベントを発生させたり、クラス固有のロジックを開始してインスタンスの要素プロパティを変更したり、イベントを処理済みとしてマークしたり、その他のイベント処理ロジックを実行したりできます。

静的クラス イベント ハンドラーとは異なり、WPF イベント システムは、クラス階層内の最も派生クラスのオーバーライド クラス イベント ハンドラーのみを呼び出します。 クラス階層内で最も派生したクラスは、基本 キーワードを使用して、仮想メソッドの基本実装を呼び出すことができます。 ほとんどの場合、イベントを処理済みとしてマークするかどうかに関係なく、基本実装を呼び出す必要があります。 基底実装ロジックを置き換える必要があるクラスがある場合にのみ、基本実装の呼び出しを省略する必要があります。 オーバーライドするコードの前または後に基本実装を呼び出すかどうかは、実装の性質によって異なります。

前のコード サンプルでは、基底クラス 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 基底クラスは ButtonBaseされ、UIElementから間接的に派生します。 制御入力処理に必要なイベント インフラストラクチャの多くは、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 を処理済みとしてマークしないように注意してください。

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

参考