プレビュー イベント (WPF .NET)

プレビュー イベント (トンネリング イベントとも呼ばれます) は、アプリケーション ルート要素からイベントを発生させた要素に向けて要素ツリーを検査するルーティング イベントです。 イベントを発生させた要素は、イベント データ内で Source として報告されます。 すべてのイベント シナリオでプレビュー イベントがサポートされているわけでも、必要なわけでもありません。 この記事では、プレビュー イベントが存在する場所と、アプリケーションまたはコンポーネントがそれらのイベントと対話する方法について説明します。 プレビュー イベントを作成する方法については、「カスタム ルーティング イベントを作成する方法」を参照してください。

重要

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

前提条件

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

処理済みとしてマークされたプレビュー イベント

プレビュー イベントをイベント データで処理済みとしてマークする場合は注意してください。 プレビュー イベントを発生させた要素以外の要素で処理済みとしてマークすると、イベントを発生させた要素がイベントを処理できなくなる可能性があります。 プレビュー イベントを意図的に処理済みとしてマークする場合もあります。 たとえば、複合コントロールでは、個々のコンポーネントによって発生したイベントを抑制し、完全なコントロールによって発生したイベントに置き換えることができます。 コントロールのカスタム イベントでは、コンポーネントの状態リレーションシップに基づいて、カスタマイズされたイベント データとトリガーを提供できます。

入力イベントの場合、イベント データは、各イベントのプレビューと非プレビュー (バブリング) の両方に相当するイベントで共有されます。 プレビュー イベント クラス ハンドラーを使用して入力イベントを処理済みとしてマークする場合、通常、バブリング入力イベントのクラス ハンドラーは呼び出されません。 また、プレビュー イベント インスタンス ハンドラーを使用してイベントを処理済みとしてマークする場合、通常、バブリング入力イベントのインスタンス ハンドラーは呼び出されません。 イベントが処理済みとしてマークされている場合でも、呼び出されるクラス ハンドラーとインスタンス ハンドラーを構成できますが、そのハンドラー構成は一般的ではありません。 クラス処理とプレビュー イベントとの関係の詳細については、「ルーティング イベントの処理済みとしてのマーキング、およびクラス処理」を参照してください。

注意

すべてのプレビュー イベントがトンネリング イベントであるわけではありません。 たとえば、PreviewMouseLeftButtonDown 入力イベントは要素ツリー内を下向きにたどりますが、ルート内の各 UIElement によって発生および再発生する直接ルーティング イベントです。

コントロールによるイベント抑制の回避

一部の複合コントロールでは、コンポーネント レベルで入力イベントを抑制し、カスタマイズされた高レベルのイベントに置き換えます。 たとえば、WPF ButtonBase では、MouseLeftButtonDown バブル入力イベントをその OnMouseLeftButtonDown メソッドで処理済みとしてマークし、Click イベントを発生させます。 MouseLeftButtonDown イベントとそのイベント データは要素ツリー ルートに沿って引き続き処理されますが、イベントはイベント データ内で Handled としてマークされているため、処理されたイベントに応答するように構成されたハンドラーのみが呼び出されます。

処理済みとしてマークされているルーティング イベントをアプリケーションのルートに向けて他の要素で処理する場合は、次のいずれかを実行できます。

  • UIElement.AddHandler(RoutedEvent, Delegate, Boolean) メソッドを呼び出し、handledEventsToo パラメーターを true に設定して、ハンドラーをアタッチします。 この方法では、アタッチ先の要素へのオブジェクト参照を取得した後に、分離コードでイベント ハンドラーをアタッチする必要があります。

  • 処理済みとしてマークされたイベントがバブリング イベントの場合は、同等のプレビュー イベントのハンドラー (使用可能な場合) をアタッチします。 たとえば、コントロールで MouseLeftButtonDown イベントが抑制されている場合は、代わりに PreviewMouseLeftButtonDown イベントのハンドラーをアタッチできます。 この方法は、トンネリングバブルの両方のルーティング方法を実装し、イベント データを共有する基本要素入力イベントに対してのみ機能します。

次の例では、TextBox を含む componentWrapper という名前の基本的なカスタム コントロールを実装します。 コントロールは outerStackPanel という名前の StackPanel に追加されます。

<StackPanel Name="outerStackPanel"
    VerticalAlignment="Center"
    custom:ComponentWrapper.CustomKey="Handler_PrintEventInfo"
    TextBox.KeyDown="Handler_PrintEventInfo"
    TextBox.PreviewKeyDown="Handler_PrintEventInfo" >
    <custom:ComponentWrapper
        x:Name="componentWrapper"
        TextBox.KeyDown="ComponentWrapper_KeyDown"
        custom:ComponentWrapper.CustomKey="Handler_PrintEventInfo"
        HorizontalAlignment="Center">
        <TextBox Name="componentTextBox" Width="200" KeyDown="Handler_PrintEventInfo" />
    </custom:ComponentWrapper>
</StackPanel>

componentWrapper コントロールは、キーストロークが発生するたびに、その TextBox コンポーネントによって発生した KeyDown バブリング イベントをリッスンします。 その場合、componentWrapper コントロールでは次の操作を行います。

  1. KeyDown バブリング ルーティング イベントを処理済みとしてマークして抑制します。 その結果、"処理済みの" KeyDown イベントに応答するように分離コードで構成された outerStackPanel ハンドラーのみがトリガーされます。 KeyDown イベントに対して XAML でアタッチされた outerStackPanel ハンドラーは呼び出されません。

  2. CustomKey イベントの outerStackPanel ハンドラーをトリガーする、CustomKey という名前のカスタム バブリング ルーティング イベントを発生させます。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // Attach a handler on outerStackPanel that will be invoked by handled KeyDown events.
        outerStackPanel.AddHandler(KeyDownEvent, new RoutedEventHandler(Handler_PrintEventInfo), 
            handledEventsToo: true);
    }

    private void ComponentWrapper_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
    {
        Handler_PrintEventInfo(sender, e);

        Debug.WriteLine("KeyDown event marked as handled on componentWrapper.\r\n" +
            "CustomKey event raised on componentWrapper.");

        // Mark the event as handled.
        e.Handled = true;

        // Raise the custom click event.
        componentWrapper.RaiseCustomRoutedEvent();
    }

    private void Handler_PrintEventInfo(object sender, System.Windows.Input.KeyEventArgs e)
    {
        string senderName = ((FrameworkElement)sender).Name;
        string sourceName = ((FrameworkElement)e.Source).Name;
        string eventName = e.RoutedEvent.Name;
        string handledEventsToo = e.Handled ? " Parameter handledEventsToo set to true." : "";

        Debug.WriteLine($"Handler attached to {senderName} " +
            $"triggered by {eventName} event raised on {sourceName}.{handledEventsToo}");
    }

    private void Handler_PrintEventInfo(object sender, RoutedEventArgs e)
    {
        string senderName = ((FrameworkElement)sender).Name;
        string sourceName = ((FrameworkElement)e.Source).Name;
        string eventName = e.RoutedEvent.Name;
        string handledEventsToo = e.Handled ? " Parameter handledEventsToo set to true." : "";

        Debug.WriteLine($"Handler attached to {senderName} " +
            $"triggered by {eventName} event raised on {sourceName}.{handledEventsToo}");
    }

    // Debug output:
    //
    // Handler attached to outerStackPanel triggered by PreviewKeyDown event raised on componentTextBox.
    // Handler attached to componentTextBox triggered by KeyDown event raised on componentTextBox.
    // Handler attached to componentWrapper triggered by KeyDown event raised on componentTextBox.
    // KeyDown event marked as handled on componentWrapper.
    // CustomKey event raised on componentWrapper.
    // Handler attached to componentWrapper triggered by CustomKey event raised on componentWrapper.
    // Handler attached to outerStackPanel triggered by CustomKey event raised on componentWrapper.
    // Handler attached to outerStackPanel triggered by KeyDown event raised on componentTextBox. Parameter handledEventsToo set to true.
}

public class ComponentWrapper : StackPanel
{
    // Register a custom routed event using the Bubble routing strategy.
    public static readonly RoutedEvent CustomKeyEvent = 
        EventManager.RegisterRoutedEvent(
            name: "CustomKey",
            routingStrategy: RoutingStrategy.Bubble,
            handlerType: typeof(RoutedEventHandler),
            ownerType: typeof(ComponentWrapper));

    // Provide CLR accessors for assigning an event handler.
    public event RoutedEventHandler CustomKey
    {
        add { AddHandler(CustomKeyEvent, value); }
        remove { RemoveHandler(CustomKeyEvent, value); }
    }

    public void RaiseCustomRoutedEvent()
    {
        // Create a RoutedEventArgs instance.
        RoutedEventArgs routedEventArgs = new(routedEvent: CustomKeyEvent);

        // Raise the event, which will bubble up through the element tree.
        RaiseEvent(routedEventArgs);
    }
}
Partial Public Class MainWindow
        Inherits Window

        Public Sub New()
        InitializeComponent()

        ' Attach a handler on outerStackPanel that will be invoked by handled KeyDown events.
        outerStackPanel.[AddHandler](KeyDownEvent, New RoutedEventHandler(AddressOf Handler_PrintEventInfo),
                                     handledEventsToo:=True)
    End Sub

    Private Sub ComponentWrapper_KeyDown(sender As Object, e As KeyEventArgs)
        Handler_PrintEventInfo(sender, e)
        Debug.WriteLine("KeyDown event marked as handled on componentWrapper." &
                        vbCrLf & "CustomKey event raised on componentWrapper.")

        ' Mark the event as handled.
        e.Handled = True

        ' Raise the custom click event.
        componentWrapper.RaiseCustomRoutedEvent()
    End Sub

    Private Sub Handler_PrintEventInfo(sender As Object, e As KeyEventArgs)
        Dim senderName As String = CType(sender, FrameworkElement).Name
        Dim sourceName As String = CType(e.Source, FrameworkElement).Name
        Dim eventName As String = e.RoutedEvent.Name
        Dim handledEventsToo As String = If(e.Handled, " Parameter handledEventsToo set to true.", "")
        Debug.WriteLine($"Handler attached to {senderName} " &
                        $"triggered by {eventName} event raised on {sourceName}.{handledEventsToo}")
    End Sub

    Private Sub Handler_PrintEventInfo(sender As Object, e As RoutedEventArgs)
        Dim senderName As String = CType(sender, FrameworkElement).Name
        Dim sourceName As String = CType(e.Source, FrameworkElement).Name
        Dim eventName As String = e.RoutedEvent.Name
        Dim handledEventsToo As String = If(e.Handled, " Parameter handledEventsToo set to true.", "")
        Debug.WriteLine($"Handler attached to {senderName} " &
                        $"triggered by {eventName} event raised on {sourceName}.{handledEventsToo}")
    End Sub

    ' Debug output
    '
    ' Handler attached to outerStackPanel triggered by PreviewKeyDown event raised on componentTextBox.
    ' Handler attached to componentTextBox triggered by KeyDown event raised on componentTextBox.
    ' Handler attached to componentWrapper triggered by KeyDown event raised on componentTextBox.
    ' KeyDown event marked as handled on componentWrapper.
    ' CustomKey event raised on componentWrapper.
    ' Handler attached to componentWrapper triggered by CustomKey event raised on componentWrapper.
    ' Handler attached to outerStackPanel triggered by CustomKey event raised on componentWrapper.
    ' Handler attached to outerStackPanel triggered by KeyDown event raised on componentTextBox. Parameter handledEventsToo set to true.
End Class

    Public Class ComponentWrapper
        Inherits StackPanel

        ' Register a custom routed event with the Bubble routing strategy.
        Public Shared ReadOnly CustomKeyEvent As RoutedEvent =
            EventManager.RegisterRoutedEvent(
                name:="CustomKey",
                routingStrategy:=RoutingStrategy.Bubble,
                handlerType:=GetType(RoutedEventHandler),
                ownerType:=GetType(ComponentWrapper))

        ' Provide CLR accessors to support event handler assignment.
        Public Custom Event CustomKey As RoutedEventHandler

            AddHandler(value As RoutedEventHandler)
                [AddHandler](CustomKeyEvent, value)
            End AddHandler

            RemoveHandler(value As RoutedEventHandler)
                [RemoveHandler](CustomKeyEvent, value)
            End RemoveHandler

            RaiseEvent(sender As Object, e As RoutedEventArgs)
                [RaiseEvent](e)
            End RaiseEvent

        End Event

    Public Sub RaiseCustomRoutedEvent()
        ' Create a RoutedEventArgs instance & raise the event,
        ' which will bubble up through the element tree.
        Dim routedEventArgs As New RoutedEventArgs(routedEvent:=CustomKeyEvent)
            [RaiseEvent](routedEventArgs)
        End Sub
    End Class

この例では、抑制された KeyDown ルーティング イベントを取得して、outerStackPanel にアタッチされたイベント ハンドラーを呼び出す 2 つの回避策を示します。

  • PreviewKeyDown イベント ハンドラーを outerStackPanel にアタッチします。 プレビュー入力ルーティング イベントは同等のバブリング ルーティング イベントに先行するため、この例の PreviewKeyDown ハンドラーは、共有イベント データを介してプレビュー イベントとバブリング イベントの両方を抑制する KeyDown ハンドラーの前に実行されます。

  • 分離コードで UIElement.AddHandler(RoutedEvent, Delegate, Boolean) メソッドを使用して KeyDown イベント ハンドラーを outerStackPanel にアタッチし、handledEventsToo パラメーターを true に設定します。

注意

入力イベントのプレビューまたはプレビュー以外に相当するイベントを処理済みとしてマークすることは、コントロールのコンポーネントによって発生するイベントを抑制するための戦略です。 使用する方法は、アプリケーションの要件によって異なります。

参照