Preview events (WPF .NET)
Preview events, also known as tunneling events, are routed events that traverse downward through the element tree from the application root element to the element that raised the event. The element that raises an event is reported as the Source in the event data. Not all event scenarios support or require preview events. This article describes where preview events exist and how applications or components can interact with them. For information on how to create a preview event, see How to create a custom routed event.
Prerequisites
The article assumes a basic knowledge of routed events, and that you've read Routed events overview. To follow the examples in this article, it helps if you're familiar with Extensible Application Markup Language (XAML) and know how to write Windows Presentation Foundation (WPF) applications.
Preview events marked as handled
Be cautious when marking preview events as handled in event data. Marking a preview event as handled on an element other than the element that raised it can prevent the element that raised it from handling the event. Sometimes marking preview events as handled is intentional. For example, a composite control might suppress events raised by individual components and replace them with events raised by the complete control. Custom events for a control can provide customized event data and trigger based on component state relationships.
For input events, event data is shared by both the preview and non-preview (bubbling) equivalents of each event. If you use a preview event class-handler to mark an input event as handled, class-handlers for the bubbling input event typically won't be invoked. Or, if you use a preview event instance-handler to mark an event as handled, instance-handlers for the bubbling input event typically won't be invoked. Although you can configure class and instance handlers to be invoked even if an event is marked as handled, that handler configuration isn't common. For more information about class handling and how it relates to preview events, see Marking routed events as handled and class handling.
Note
Not all preview events are tunneling events. For example, the PreviewMouseLeftButtonDown input event follows a downward route through the element tree, but is a direct routed event that's raised and reraised by each UIElement in the route.
Working around event suppression by controls
Some composite controls suppress input events at the component-level in order to replace them with a customized high-level event. For example, the WPF ButtonBase marks the MouseLeftButtonDown bubbling input event as handled in its OnMouseLeftButtonDown method and raises the Click event. The MouseLeftButtonDown
event and its event data still continue along the element tree route, but because the event is marked as Handled in event data, only handlers that are configured to respond to handled events are invoked.
If you want other elements toward the root of your application to handle a routed event that's marked as handled, you can either:
Attach handlers by calling the UIElement.AddHandler(RoutedEvent, Delegate, Boolean) method and setting the parameter
handledEventsToo
totrue
. This approach requires attaching the event handler in code-behind, after obtaining an object reference to the element that it will attach to.If the event marked as handled is a bubbling event, attach handlers for the equivalent preview event if available. For instance, if a control suppresses the MouseLeftButtonDown event, you can attach a handler for the PreviewMouseLeftButtonDown event instead. This approach only works for base element input events that implement both tunneling and bubbling routing strategies and share event data.
The following example implements a rudimentary custom control named componentWrapper
that contains a TextBox. The control is added to a StackPanel named outerStackPanel
.
<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>
The componentWrapper
control listens for the KeyDown bubbling event raised by its TextBox
component whenever a keystroke occurs. On that occurrence, the componentWrapper
control:
Marks the
KeyDown
bubbling routed event as handled to suppress it. As a result, only theouterStackPanel
handler that's configured in code-behind to respond to handledKeyDown
events is triggered. TheouterStackPanel
handler attached in XAML forKeyDown
events isn't invoked.Raises a custom bubbling routed event named
CustomKey
, which triggers theouterStackPanel
handler for theCustomKey
event.
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
The example demonstrates two workarounds for getting the suppressed KeyDown
routed event to invoke an event handler attached to the outerStackPanel
:
Attach a PreviewKeyDown event handler to the
outerStackPanel
. Since a preview input routed event precedes the equivalent bubbling routed event, thePreviewKeyDown
handler in the example runs ahead of theKeyDown
handler that suppresses both preview and bubbling events through their shared event data.Attach a
KeyDown
event handler to theouterStackPanel
by using the UIElement.AddHandler(RoutedEvent, Delegate, Boolean) method in code-behind, with thehandledEventsToo
parameter set totrue
.
Note
Marking preview or non-preview equivalents of input events as handled are both strategies for suppressing events raised by the components of a control. The approach you use depends on your application requirements.
See also
.NET Desktop feedback