將路由事件標記為已處理以及類別處理 (WPF .NET)
雖然沒有絕對規則規定何時要將路由事件標示為已處理,但如果您的程式碼以顯著的方式回應事件,請考慮將事件標示為已處理。 標示為已處理的路由事件會沿著其路由繼續,但只會叫用設定為回應已處理事件的處理常式。 基本上,將路由事件標示為已處理,會限制其在事件路由上的接聽程序可見度。
路由事件處理常式可以是實例處理常式或類別處理常式。 實例處理常式會處理物件或 XAML 元素上的路由事件。 類別處理常式會處理類別層級的路由事件,並在任何實例處理常式對該類別任何實例上的相同事件做出回應之前叫用。 當路由事件標示為已處理時,其通常也會在類別處理常式中標示為已處理。 本文討論將路由事件標示為已處理的優點和潛在陷阱、不同類型的路由事件和路由事件處理常式,以及複合控制項中的事件歸併。
必要條件
本文假設您已基本了解路由事件,而且您已閱讀路由事件概觀。 若要遵循本文中的範例,建議您先熟悉 Extensible Application Markup Language (XAML),並了解如何撰寫 Windows Presentation Foundation (WPF) 應用程式。
何時將路由事件標示為已處理
一般而言,只有一個處理常式應該為每個路由事件提供重要的回應。 避免使用路由事件系統在多個處理常式之間提供重大回應。 構成重大回應的定義是主觀的,取決於您的應用程式。 一般指引:
- 重大回應包括設定焦點、修改公用狀態、設定影響視覺表示的屬性、引發新事件,以及完全處理事件。
- 微不足道的回應包括修改私人狀態且不造成視覺或程式設計影響、事件記錄,以及檢查事件資料而不回應事件。
某些 WPF 控制項會隱藏不需要進一步處理的元件層級事件,方法是將其標示為已處理。 如果您想要處理標示為已由控制項處理的事件,請參閱處理控制項的事件歸併。
若要將事件標示為已處理,請將其事件資料中的 Handled 屬性值設定為 true
。 雖然可以將該值還原為 false
,但這樣做的需求應該很少見。
預覽和事件反昇路由事件配對
預覽和事件反昇路由事件配對是輸入事件特有的。 數個輸入事件會實作通道和事件反昇路由事件配對,例如 PreviewKeyDown 和 KeyDown。 Preview
字首表示一旦完成預覽事件便會啟動事件反昇事件。 每個預覽和事件反昇事件配對都會共用相同的事件資料實例。
路由事件處理常式的叫用順序會對應至事件的路由策略:
- 預覽事件會從應用程式根項目向下移至引發路由事件的元素。 會先叫用附加至應用程式根項目的預覽事件處理常式,然後再叫用附加至後續巢狀元素的處理常式。
- 預覽事件完成之後,配對的事件反昇事件會從引發路由事件的元素移至應用程式根項目。 會先叫用附加至引發路由事件的相同元素的事件反昇事件處理常式,然後再叫用附加至後續父元素的處理常式。
配對的預覽和事件反昇事件是數個 WPF 類別的內部實作的一部分,這些類別會宣告和引發自己的路由事件。 如果沒有該類別層級的內部實作,不論事件命名為何,預覽和事件反昇路由事件都會完全分開,也不會共用事件資料。 如需如何在自訂類別中實作事件反昇或通道輸入路由事件的相關資訊,請參閱建立自訂路由事件。
因為每個預覽和事件反昇事件組都會共用相同的事件資料實例,因此如果預覽路由事件標示為已處理,則也會處理其配對的反升事件反昇事件。 如果事件反昇路由事件標示為已處理,則不會影響配對的預覽事件,因為預覽事件已完成。 將預覽和事件反昇輸入事件配對標示為已處理時,請小心。 已處理的預覽輸入事件不會針對通道路由的其餘部分叫用任何一般註冊的事件處理常式,而且不會引發配對的事件反昇事件。 已處理的事件反昇輸入事件不會針對事件反昇路由的其餘部分叫用任何一般註冊的事件處理常式。
實例和類別路由事件處理常式
路由事件處理常式可以是實例處理常式或類別處理常式。 指定類別的類別處理常式,會在任何實例處理常式對該類別任何實例上的相同事件做出回應之前叫用。 由於此行為,當路由事件標示為已處理時,其通常也會在類別處理常式中標示為已處理。 類別處理常式有兩種類型:
- 靜態類別事件處理常式,其註冊方式為在靜態類別建構函式內呼叫 RegisterClassHandler 方法。
- 覆寫類別事件處理常式,其註冊方式為覆寫基底類別虛擬事件方法。 基底類別虛擬事件方法主要存在於輸入事件中,且名稱開頭為 On<event name> 以及 OnPreview<event name>。
實例事件處理常式
您可以直接呼叫 AddHandler 方法,將實例處理常式附加至物件或 XAML 元素。 WPF 路由事件會實作通用語言執行平台 (CLR) 事件包裝函式,該包裝函式使用 AddHandler
方法來附加事件處理常式。 由於用於附加事件處理常式的 XAML 屬性語法會造成呼叫 CLR 事件包裝函式,因此即使是在 XAML 中附加處理常式也會解析為 AddHandler
呼叫。 對於已處理的事件:
- 不會叫用使用 XAML 屬性語法或
AddHandler
的一般簽章來進行附加的處理常式。 - 系統會叫用使用 AddHandler(RoutedEvent, Delegate, Boolean) 多載進行附加的處理常式,並將
handledEventsToo
參數設定為true
。 當需要回應已處理的事件時,此多載就可用於罕見的情況。 例如,元素樹狀結構中的某些元素已將事件標示為已處理,但事件路由上的其他元素需要回應已處理的事件。
下列 XAML 範例將名為 componentWrapper
的自訂控制項新增至名為 outerStackPanel
的 StackPanel,該控制項包裝名為 componentTextBox
的 TextBox。 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 方法,以附加靜態類別事件處理常式。 類別階層中的每個類別都可以針對每個路由事件註冊自己的靜態類別處理常式。 因此,在事件路由中的任何指定節點上,可以針對相同的事件叫用多個靜態類別處理常式。 建構事件的事件路由時,每個節點的所有靜態類別處理常式都會新增至事件路由。 節點上靜態類別處理常式的叫用順序從衍生程度最大的靜態類別處理常式開始,後面接著每個後續基底類別的靜態類別處理常式。
使用 RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) 多載註冊並將 handledEventsToo
參數設定為 true
的靜態類別事件處理常式,會回應未處理和已處理的路由事件。
靜態類別處理常式通常會註冊為只回應未處理的事件。 在此情況下,如果節點上的衍生類別處理常式將事件標示為已處理,則不會為該事件叫用基底類別處理常式。 在該案例中,基底類別處理常式實際上會被衍生類別處理常式取代。 基底類別處理常式通常會在視覺外觀、狀態邏輯、輸入處理和命令處理等區域中參與控制項設計,因此取代時請謹慎。 不將事件標示為已處理的衍生類別處理常式最終會補充基底類別處理常式,而不是取代它們。
下列程式碼範例顯示上述 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<event name> 和 OnPreview<event name> 虛擬方法。 例如,UIElement 實作 OnKeyDown 和 OnPreviewKeyDown 虛擬事件處理常式,以及許多其他事件處理常式。 您可以覆寫基底類別虛擬事件處理常式,以實作衍生類別的覆寫類別事件處理常式。 例如,您可以覆寫 OnDragEnter 虛擬方法,為任何 UIElement
衍生類別中的 DragEnter 事件新增覆寫類別處理常式。 若要實作類別處理常式,相較於在靜態建構函式中註冊類別處理常式,覆寫基底類別虛擬方法簡單多了。 在覆寫中,您可以引發事件、起始類別特定的邏輯來變更實例上的元素屬性、將事件標示為已處理,或執行其他事件處理邏輯。
不同於靜態類別事件處理常式,WPF 事件系統只會針對類別階層中衍生程式最大的類別,叫用覆寫類別事件處理常式。 然後,類別階層中衍生程式最大的類別,可使用基底關鍵字來呼叫虛擬方法的基底實作。 在大部分情況下,您應該呼叫基底實作,不論是否將事件標示為已處理。 如果您的類別需要取代基底實作邏輯 (如果有的話),則應該只省略呼叫基底實作。 要先覆寫程式碼再呼叫基底實作,或先呼叫基底實作再覆寫程式碼,視您實作的性質而定。
在上述程式碼範例中,會覆寫 ComponentWrapper
和 ComponentWrapperBase
類別中的基底類別 OnKeyDown
虛擬方法。 由於 WPF 事件系統只會叫用 ComponentWrapper.OnKeyDown
覆寫類別事件處理常式,因此該處理常式會使用 base.OnKeyDown(e)
來呼叫 ComponentWrapperBase.OnKeyDown
覆寫類別事件處理常式,進而使用 base.OnKeyDown(e)
來呼叫 StackPanel.OnKeyDown
虛擬方法。 上述程式碼範例中的事件順序為:
- 附加至
componentWrapper
的實例處理常式由PreviewKeyDown
路由事件所觸發。 - 附加至
componentWrapper
的靜態類別處理常式由KeyDown
路由事件所觸發。 - 附加至
componentWrapper
的靜態類別處理常式由KeyDown
路由事件所觸發。 - 附加至
componentWrapper
的覆寫類別處理常式由KeyDown
路由事件所觸發。 - 附加至
componentWrapper
的覆寫類別處理常式由KeyDown
路由事件所觸發。 KeyDown
路由事件標示為已處理。- 附加至
componentWrapper
的實例處理常式由PreviewKeyDown
路由事件所觸發。 處理常式已註冊,且handledEventsToo
參數設定為true
。
複合控制項中的輸入事件歸併
某些複合控制項會在元件層級隱藏輸入事件,以便用自訂的高階事件加以取代,其中包含更多資訊或暗示更特定的行為。 從定義上來看,複合控制項是由多個實用的控制項或控制項基底類別組成。 傳統範例是 Button 控制項,其會將各種滑鼠事件轉換成 Click 路由事件。 Button
基底類別為 ButtonBase,間接衍生自 UIElement。 控制項輸入處理所需的大部分事件基礎結構都可在 UIElement
層級取得。 UIElement
會公開數個 Mouse 事件,例如 MouseLeftButtonDown 和 MouseRightButtonDown。 UIElement
也會實作空的虛擬方法 OnMouseLeftButtonDown 和 OnMouseRightButtonDown,作為預先註冊的類別處理常式。 ButtonBase
會覆寫這些類別處理常式,並在覆寫處理常式內將 Handled 屬性設定為 true
,並引發 Click
事件。 大部分接聽程式的最終結果是隱藏 MouseLeftButtonDown
和 MouseRightButtonDown
事件,並顯示高階 Click
事件。
處理輸入事件歸併
有時候個別控制項內的事件歸併可能會干擾應用程式中的事件處理邏輯。 例如,如果您的應用程式使用 XAML 屬性語法為 XAML 根項目上的 MouseLeftButtonDown 事件附加處理常式,則不會叫用該處理常式,因為 Button 控制項會將 MouseLeftButtonDown
事件標示為已處理。 如果您想要針對已處理的路由事件叫用應用程式根的元素,您可以:
呼叫 UIElement.AddHandler(RoutedEvent, Delegate, Boolean) 方法,並將
handledEventsToo
參數設定為true
來附加處理常式。 此方法需要先獲得要附加至其中的元素的物件參考,然後再以程式碼後置的方式附加事件處理常式。如果標示為已處理的事件是事件反昇輸入事件,則請附加配對預覽事件的處理常式 (如果有的話)。 例如,如果控制項隱藏
MouseLeftButtonDown
事件,您可以改為附加 PreviewMouseLeftButtonDown 事件的處理常式。 此方法僅適用於共用事件資料的預覽和事件反昇輸入事件配對。 請小心不要將PreviewMouseLeftButtonDown
標示為已處理,因為這樣會完全隱藏 Click 事件。
如需如何處理輸入事件歸併的範例,請參閱處理控制項的事件歸併。