將路由事件標示為已處理,以及類別處理 (WPF .NET)

雖然何時將路由事件標示為已處理的絕對規則,但如果您的程式碼以重要方式回應事件,請考慮將事件標示為已處理。 標示為已處理的路由事件會沿著其路由繼續,但只會叫用設定為回應已處理事件的處理常式。 基本上,將路由事件標示為已處理,會限制其在事件路由上的接聽程式可見度。

路由事件處理常式可以是實例處理常式或類別處理常式。 實例處理常式會處理物件或 XAML 元素上的路由事件。 類別處理常式會處理類別層級的路由事件,並在回應類別任何實例上相同事件的任何實例處理常式之前叫用。 當路由事件標示為已處理時,通常會在類別處理常式中標示為這類事件。 本文討論將路由事件標示為已處理的優點和潛在陷阱、不同類型的路由事件和路由事件處理常式,以及複合控制項中的事件歸併。

重要

.NET 7 和 .NET 6 的桌面指南檔正在建置中。

必要條件

本文假設您已瞭解路由事件,而且您已閱讀 路由事件概觀 。 若要遵循本文中的範例,如果您熟悉可延伸的應用程式標記語言(XAML),並知道如何撰寫 Windows Presentation Foundation (WPF) 應用程式,它很有説明。

何時將路由事件標示為已處理

一般而言,只有一個處理常式應該為每個路由事件提供重要的回應。 避免使用路由事件系統在多個處理常式之間提供重大回應。 構成重大回應的定義是主觀的,取決於您的應用程式。 作為一般指引:

  • 重大回應包括設定焦點、修改公用狀態、設定影響視覺表示的屬性、引發新事件,以及完全處理事件。
  • 微不足道的回應包括修改私用狀態,而不需要視覺或程式設計影響、事件記錄,以及檢查事件資料而不回應事件。

某些 WPF 控制項會隱藏不需要進一步處理的元件層級事件,方法是將其標示為已處理。 如果您想要處理標示為控制項所處理的事件,請參閱 解決控制項 的事件隱藏。

若要將事件標示為 已處理 ,請將 Handled 其事件資料中的 屬性值設定為 true 。 雖然可以將該值還原為 false ,但這樣做的需求應該很少見。

預覽和反升路由事件組

預覽 和反升路由事件組是輸入事件 特有的 。 數個 輸入事件會實作通道和 反升 路由事件組,例如 PreviewKeyDownKeyDown 。 前置 Preview 詞表示一旦預覽事件完成,就會啟動反升事件。 每個預覽和反升事件組都會共用相同的事件資料實例。

路由事件處理常式的叫用順序會對應至事件的路由策略:

  1. 預覽事件會從應用程式根項目向下移至引發路由事件的專案。 附加至應用程式根項目的預覽事件處理常式會先叫用,後面接著附加至後續巢狀元素的處理常式。
  2. 預覽事件完成之後,配對的反升事件會從引發路由事件至應用程式根項目的專案移動。 附加至引發路由事件之相同元素的布布林事件處理常式會先叫用,後面接著附加至後續父元素的處理常式。

配對的預覽和反升事件是數個 WPF 類別的內部實作的一部分,這些類別會宣告和引發自己的路由事件。 如果沒有該類別層級的內部實作,不論事件命名為何,預覽和反升路由事件是完全分開的,也不會共用事件資料。 如需如何在自訂類別中實作布建或通道輸入路由事件的相關資訊,請參閱 建立自訂路由事件

因為每個預覽和泡泡事件組都會共用相同的事件資料實例,因此如果預覽路由事件標示為已處理,則也會處理其配對的反升事件。 如果反升路由事件標示為已處理,則不會影響配對的預覽事件,因為預覽事件已完成。 將預覽和反升輸入事件配對標示為已處理時,請小心。 已處理的預覽輸入事件不會針對通道路由的其餘部分叫用任何一般註冊的事件處理常式,而且不會引發配對的反升事件。 已處理的反升輸入事件不會針對反升路由的其餘部分叫用任何一般註冊的事件處理常式。

實例和類別路由事件處理常式

路由事件處理常式可以是 實例 處理常式或 類別 處理常式。 指定類別的類別處理常式會在回應該類別的任何實例上回應相同事件的任何實例處理常式之前叫用。 由於此行為,當路由事件標示為已處理時,通常會在類別處理常式中標示為這類。 類別處理常式有兩種類型:

實例事件處理常式

您可以直接呼叫 AddHandler 方法,將實例處理常式附加至物件或 XAML 元素。 WPF 路由事件會實作 Common Language Runtime (CLR) 事件包裝函式,該包裝函式會使用 AddHandler 方法來附加事件處理常式。 由於附加事件處理常式的 XAML 屬性語法會導致呼叫 CLR 事件包裝函式,即使是 XAML 中的附加處理常式也會解析為 AddHandler 呼叫。 針對已處理的事件:

  • 不會叫用使用 XAML 屬性語法或 的一般簽章 AddHandler 附加的處理常式。
  • 使用 AddHandler(RoutedEvent, Delegate, Boolean) 多載附加的處理常式,並叫用 設定為 handledEventsTootrue 的參數。 當需要回應已處理的事件時,此多載就可用於罕見的情況。 例如,元素樹狀結構中的某些專案已將事件標示為已處理,但事件路由上的其他元素需要回應已處理的事件。

下列 XAML 範例會將名為 componentWrapper 的自訂控制項,將具名 的 包裝 TextBoxStackPanel 具名 componentTextBoxouterStackPanel 的 。 事件的實例事件處理常式 PreviewKeyDown 會附加至 componentWrapper 使用 XAML 屬性語法的 。 因此,實例處理常式只會回應 所 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 方法,以附加靜態類別事件處理常式。 類別階層中的每個類別都可以為每個路由事件註冊自己的靜態類別處理常式。 因此,在事件路由中的任何指定節點上,可以針對相同的事件叫用多個靜態類別處理常式。 建構事件的事件路由時,每個節點的所有靜態類別處理常式都會新增至事件路由。 節點上靜態類別處理常式的叫用順序會以衍生最多的靜態類別處理常式開始,後面接著每個後續基類的靜態類別處理常式。

使用 多載註冊的靜態類別事件處理常式, RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) 並將 handledEventsToo 參數設定為 true ,將回應未處理的路由事件和已處理的路由事件。

靜態類別處理常式通常會註冊,只回應未處理的事件。 在此情況下,如果節點上的衍生類別處理常式將事件標示為已處理,則不會叫用該事件的基類處理常式。 在該案例中,基類處理常式實際上會由衍生類別處理常式取代。 基類處理常式通常會在視覺外觀、狀態邏輯、輸入處理和命令處理等區域中參與控制設計,因此請謹慎取代它們。 未將事件標示為已處理的衍生類別處理常式最終會補充基類處理常式,而不是取代它們。

下列程式碼範例顯示上述 XAML 中所參考之自訂控制項的類別階層 ComponentWrapperComponentWrapper 類別衍生自 ComponentWrapperBase 類別,而後者又會衍生自 StackPanel 類別。 和 RegisterClassHandler 類別的 ComponentWrapperComponentWrapperBase 靜態建構函式中使用的 方法會註冊每個類別的靜態類別事件處理常式。 WPF 事件系統會在 ComponentWrapper 靜態類別處理常式之前 ComponentWrapperBase 叫用靜態類別處理常式。

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 虛擬事件處理常式,以及其他許多 。 您可以覆寫基類虛擬事件處理常式,以實作衍生類別的覆寫類別事件處理常式。 例如,您可以覆寫虛擬方法,為 DragEnter 任何 UIElement 衍生類別中的 事件新增覆寫類別 OnDragEnter 處理常式。 覆寫基類虛擬方法比在靜態建構函式中註冊類別處理常式更簡單的方式。 在覆寫中,您可以引發事件、起始類別特定的邏輯來變更實例上的專案屬性、將事件標示為已處理,或執行其他事件處理邏輯。

不同于靜態類別事件處理常式,WPF 事件系統只會針對類別階層中衍生類別叫用覆寫類別事件處理常式。 類別階層中衍生的類別,接著 可以使用 base 關鍵字來呼叫虛擬方法的基底 實作。 在大部分情況下,您應該呼叫基底實作,不論是否將事件標示為已處理。 如果您的類別需要取代基底實作邏輯,則應該只省略呼叫基底實作。 不論您在覆寫程式碼之前或之後呼叫基底實作,都取決於實作的性質。

在上述程式碼範例中,基類 OnKeyDown 虛擬方法會在 和 ComponentWrapperBase 類別中 ComponentWrapper 覆寫。 由於 WPF 事件系統只會叫 ComponentWrapper.OnKeyDown 用覆寫類別事件處理常式,因此該處理常式會使用 base.OnKeyDown(e) 來呼叫 ComponentWrapperBase.OnKeyDownStackPanel.OnKeyDown 覆寫類別事件處理常式,進而使用 base.OnKeyDown(e) 呼叫虛擬方法。 上述程式碼範例中的事件順序為:

  1. 附加至 componentWrapperPreviewKeyDown 實例處理常式是由路由事件觸發。
  2. 附加至 componentWrapperKeyDown 靜態類別處理常式是由路由事件觸發。
  3. 附加至 componentWrapperBaseKeyDown 靜態類別處理常式是由路由事件觸發。
  4. 附加至 componentWrapperKeyDown 覆寫類別處理常式是由路由事件觸發。
  5. 附加至 componentWrapperBaseKeyDown 覆寫類別處理常式是由路由事件觸發。
  6. 路由 KeyDown 事件會標示為已處理。
  7. 附加至 componentWrapperKeyDown 實例處理常式是由路由事件觸發。 處理常式已向 handledEventsToo 設定為 true 的參數註冊。

複合控制項中的輸入事件歸併

某些複合控制項會隱藏 元件層級的輸入事件 ,以自訂的高階事件取代它們,其中包含更多資訊或暗示更特定的行為。 複合控制項的定義是由多個實際控制項或控制項基類所組成。 傳統範例是 Button 控制項,它會將各種滑鼠事件 Click 轉換成路由事件。 基 Button 類是 ButtonBase ,其間接衍生自 UIElement 。 控制輸入處理所需的大部分事件基礎結構都可在 UIElement 層級取得。 UIElement 會公開數 Mouse 個事件,例如 MouseLeftButtonDownMouseRightButtonDownUIElement 也會實作空的虛擬方法 OnMouseLeftButtonDown ,並 OnMouseRightButtonDown 作為預先註冊的類別處理常式。 ButtonBase 會覆寫這些類別處理常式,並在覆寫處理常式內將 屬性設定 Handledtrue ,並引發 Click 事件。 大部分接聽程式的最終結果是 MouseLeftButtonDown 隱藏 和 MouseRightButtonDown 事件,並顯示高階 Click 事件。

處理輸入事件歸併

有時候個別控制項內的事件歸併可能會干擾應用程式中的事件處理邏輯。 例如,如果您的應用程式使用 XAML 屬性語法附加 XAML 根項目上事件的處理常式 MouseLeftButtonDown ,則不會叫用該處理常式,因為 Button 控制項會將事件標示 MouseLeftButtonDown 為已處理。 如果您想要針對已處理路由事件叫用應用程式根目錄的專案,您可以:

  • 呼叫 方法 handledEventsTooUIElement.AddHandler(RoutedEvent, Delegate, Boolean) 並將 參數設定為 true ,以附加處理常式。 這個方法需要在程式碼後置中附加事件處理常式,才能取得要附加之專案的物件參考。

  • 如果標示為已處理的事件是反升輸入事件,則如果有的話,附加配對預覽事件的處理常式。 例如,如果控制項隱藏 MouseLeftButtonDown 事件,您可以改為附加事件的處理常式 PreviewMouseLeftButtonDown 。 這個方法僅適用于共用事件資料的預覽和反升輸入事件組。 請小心不要將 標示 PreviewMouseLeftButtonDown 為已處理,因為這樣會完全隱藏 Click 事件。

如需如何解決輸入事件隱藏的範例,請參閱 透過控制項 解決事件隱藏。

另請參閱