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