Routed Events & Input
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
WPF replaces the plain .NET event with the routed event: an event that is raised on one element but travels along the element tree, so that a parent can react to something that happened on a child without knowing which child. Mouse, keyboard, stylus and touch input are all delivered this way, which is why a single handler on a window can see every click inside it. This page was written against .NET 10 LTS (latest patch 10.0.13) with C# 14, Visual Studio 2026 18.11.1, Windows App SDK 2.5.1 and CommunityToolkit.Mvvm 8.4.2 on Windows 10 and Windows 11. .NET 11 is at release candidate 2 (11.0.0-rc.2) and general availability is expected at .NET Conf, 10 to 13 November 2026; the routing model has not changed since .NET Framework 3.0 and nothing on this page depends on the .NET version. WPF is the stack in focus; WinUI 3 and Windows Forms are compared in a table further down.
The running scenario is Bookshelf Desktop, with a BooksViewModel, a BookDetailsViewModel and an IDialogService.
The examples use a book list made of cards, a search box, a drag-to-resize cover preview and a rating control. How events
become commands (ICommand, RoutedCommand, KeyBinding) is the subject of the Commands page; the dependency
properties that back many of the events here are on Dependency Properties & Attached Properties. Here the focus is on
the event route itself and on the input system underneath it.
The routed event model
A routed event is an instance of the RoutedEvent class, registered once per type with
EventManager.RegisterRoutedEvent and exposed as a static read-only field (UIElement.MouseDownEvent,
ButtonBase.ClickEvent). The CLR event you see in code (MouseDown, Click) is a thin wrapper that calls
AddHandler and RemoveHandler on the element. When the event is raised, WPF builds the event route, a list of
elements and handlers, and invokes the handlers in order. The route follows the visual tree for UIElement and the
logical tree for content elements. The RoutingStrategy decides how the route is walked.
| Strategy | Route | Typical use |
|---|---|---|
|
Starts at the element that raised the event and walks up to the root. |
Most input and action events: |
|
Starts at the root and walks down to the element that raised the event. By convention the name starts with |
|
|
Only the source element’s own handlers run, like a normal CLR event. Class handlers still run, and the event can be used
in an |
|
Most WPF input is a pair: a tunnelling Preview event, raised first, and a bubbling event with the same name
without the prefix, raised second. The two share one RoutedEventArgs instance for the input system’s events, so a
handler that sets Handled to true during the tunnel phase also suppresses the bubbling partner. The figure shows a
click on the title of a book card inside the details pane of the main window.
The figure uses the title TextBlock as the element under the pointer and a plain Border as the card, so nothing on
the route marks the event handled. If the card were a Button, its class handler would mark MouseLeftButtonDown
handled to start its Click gesture, which also marks the bubbling MouseDown handled, so only the Preview lines would
reach handlers that were not registered for handled events (see the class handling section below).
Source and OriginalSource
RoutedEventArgs has two properties that are easy to confuse. Source is the element that raised the event as seen by
the element whose handler is running, and OriginalSource is the element found by hit testing or the lowest-level
object that raised it. Composite controls re-target Source so that their internals stay private: a click on the text
inside a Button reaches an outer handler with Source set to the Button (not the inner ContentPresenter or
TextBlock), while OriginalSource can be the inner visual. A handler should use sender for "the element I am
attached to", Source for "the logical control" and OriginalSource only for diagnostics or low-level hit testing.
Handling a routed event
The XAML attribute for a routed event looks like any event attribute and the handler has the usual
(object sender, EventArgs e) shape. The next example logs both phases of the click in the figure above and shows that
a parent can observe events that a child produced.
<Window x:Class="Bookshelf.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Bookshelf Desktop"
PreviewMouseDown="OnWindowPreviewMouseDown"
MouseDown="OnWindowMouseDown">
<Grid x:Name="DetailsPane"
PreviewMouseDown="OnPaneEvent"
MouseDown="OnPaneEvent">
<Border x:Name="BookCard"
Background="Transparent"
PreviewMouseDown="OnPaneEvent"
MouseDown="OnPaneEvent">
<TextBlock Text="The Pragmatic Programmer" />
</Border>
</Grid>
</Window>
using System.Diagnostics;
using System.Windows;
using System.Windows.Input;
namespace Bookshelf.Desktop;
public partial class MainWindow : Window
{
public MainWindow() => InitializeComponent();
private void OnWindowPreviewMouseDown(object sender, MouseButtonEventArgs e) =>
Debug.WriteLine($"Tunnel {e.RoutedEvent.Name} at Window: source={e.Source.GetType().Name}, original={e.OriginalSource.GetType().Name}");
private void OnWindowMouseDown(object sender, MouseButtonEventArgs e) =>
Debug.WriteLine($"Bubble {e.RoutedEvent.Name} at Window: handled={e.Handled}");
private void OnPaneEvent(object sender, MouseButtonEventArgs e) =>
Debug.WriteLine($"{e.RoutedEvent.RoutingStrategy,-7} {e.RoutedEvent.Name} at {((FrameworkElement)sender).Name}");
}
Source: Routed events overview (WPF)
Clicking the text prints the PreviewMouseDown lines in tunnel order (Window, Grid, Border), then the MouseDown lines
in bubble order (Border, Grid, Window). sender is always the element the handler is attached to; e.Source is the
element that raised the event as seen by the handler, here the TextBlock, because a Border does not re-target
Source the way a composite control such as Button does.
Handled, class handling and handledEventsToo
RoutedEventArgs.Handled is a flag, not a cancellation token: setting it to true does not stop the route. WPF keeps
walking, but by default it skips handlers that were not registered to see handled events. That default is why a
parent’s MouseDown handler stops firing as soon as any element between the source and the parent marks the event
handled. It also hides a common surprise: a handler written in XAML never runs because a control set Handled first.
Class handling
Every control type can register class handlers with EventManager.RegisterClassHandler. A class handler runs
before any instance handler on the same element, and it is how controls implement behavior. Button marks
MouseLeftButtonDown handled to begin its click gesture, TextBox handles KeyDown to edit text, and ListBox handles
mouse and key events to change selection. The consequence is that MouseLeftButtonDown or KeyDown handlers declared in
XAML on a Button, TextBox or ListBoxItem often do not fire.
| Need | Approach | Notes |
|---|---|---|
React to the click on a |
Handle |
|
See raw input on a control that handles it |
Use the |
The tunnel phase starts above the control, so it runs before the control’s class handler. |
Run an instance handler even though the event is already handled |
|
XAML attributes cannot set |
Stop a control from doing its default action |
Set |
Because the pair shares one handled flag, the control’s bubbling class handler is suppressed too. |
Replace or extend behavior for every instance of a type |
|
Runs before instance handlers. Never remove the registration; it is per type for the life of the app. |
Observing handled events
The search box in Bookshelf Desktop must clear itself when the user presses Escape, but TextBox is happy to consume many
key presses first. The following code registers a handler that sees keys even when the TextBox marked them handled,
and a class handler that makes every TextBox in the app select its whole text when it receives keyboard focus.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Bookshelf.Desktop;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Instance handler that also runs for handled events.
SearchBox.AddHandler(
UIElement.KeyDownEvent,
new KeyEventHandler(OnSearchKeyDown),
handledEventsToo: true);
}
static MainWindow()
{
// Class handler: runs for every TextBox in the process, before instance handlers.
EventManager.RegisterClassHandler(
typeof(TextBox),
UIElement.GotKeyboardFocusEvent,
new KeyboardFocusChangedEventHandler(OnAnyTextBoxGotFocus));
}
private void OnSearchKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
SearchBox.Clear();
e.Handled = true;
}
}
private static void OnAnyTextBoxGotFocus(object sender, KeyboardFocusChangedEventArgs e) =>
((TextBox)sender).SelectAll();
}
Event setters and triggers
A Style can attach handlers to every element it styles with an EventSetter, which is the XAML way to share one
handler across a whole list of items. An EventTrigger reacts to a routed event but, in WPF, can only start a
storyboard (it cannot call a method or set a property); property changes use Trigger or DataTrigger instead, which
are described on the styles and triggers page.
<Style TargetType="ListBoxItem">
<EventSetter Event="MouseDoubleClick" Handler="OnBookItemDoubleClick" />
</Style>
Source: Routed events overview (WPF)
Attached events and event delegation
An attached event is a routed event that is owned by one class but can be handled on any element. The XAML syntax is
OwnerType.EventName="Handler", and it is what makes event delegation possible. Instead of subscribing every
Button in a DataTemplate, the container handles Button.Click once and reads the originating item from
DataContext. Mouse.MouseDown, Keyboard.GotKeyboardFocus and Validation.Error are other attached events defined by
the framework.
<ListBox x:Name="BooksList"
ItemsSource="{Binding Books}"
ButtonBase.Click="OnBookActionClick">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Title}" Margin="0,0,12,0" />
<Button Content="Borrow" Tag="borrow" />
<Button Content="Details" Tag="details" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
private async void OnBookActionClick(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is not Button { Tag: string action, DataContext: BookViewModel book })
{
return;
}
var viewModel = (BooksViewModel)DataContext;
switch (action)
{
case "borrow":
await viewModel.BorrowCommand.ExecuteAsync(book);
break;
case "details":
await viewModel.ShowDetailsCommand.ExecuteAsync(book);
break;
}
}
Source: Attached events overview (WPF)
This is code-behind, so use it for view-only decisions. An action that changes data belongs behind a command on the view
model (see the Commands page); the example forwards to commands generated by [RelayCommand] rather than doing the
work itself. async void is acceptable here only because the method is an event handler.
Custom routed events
A control that should tell its parents something (BookCard was opened, the rating changed) exposes a custom routed
event. The recipe has three parts: register the event, expose a CLR wrapper, and raise it with RaiseEvent. Use the
bubbling strategy for notifications meant for ancestors, and a Tunnel event only when you also provide a bubbling
partner, following the Preview naming convention.
using System.Windows;
using System.Windows.Controls;
namespace Bookshelf.Desktop.Controls;
public class RatingControl : Control
{
public static readonly RoutedEvent RatingChangedEvent = EventManager.RegisterRoutedEvent(
name: nameof(RatingChanged),
routingStrategy: RoutingStrategy.Bubble,
handlerType: typeof(RoutedPropertyChangedEventHandler<int>),
ownerType: typeof(RatingControl));
public static readonly DependencyProperty RatingProperty = DependencyProperty.Register(
nameof(Rating),
typeof(int),
typeof(RatingControl),
new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnRatingChanged));
public event RoutedPropertyChangedEventHandler<int> RatingChanged
{
add => AddHandler(RatingChangedEvent, value);
remove => RemoveHandler(RatingChangedEvent, value);
}
public int Rating
{
get => (int)GetValue(RatingProperty);
set => SetValue(RatingProperty, value);
}
private static void OnRatingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (RatingControl)d;
control.RaiseEvent(new RoutedPropertyChangedEventArgs<int>(
(int)e.OldValue, (int)e.NewValue, RatingChangedEvent));
}
}
Source: Create a custom routed event
A parent such as the details pane then handles it with RatingControl.RatingChanged="OnRatingChanged" in XAML,
whether the RatingControl is a direct child or buried in a template. The registration name must be unique per owner
type, and the CLR wrapper name must equal the registered name so XAML can resolve it. A custom event on a type that is
not a UIElement or ContentElement can still be routed but can only be raised through an element, so most custom events
live on controls.
Weak events
Subscribing a short-lived object to a long-lived source (a view model subscribing to a static or application-wide event)
leaks the subscriber, because the source holds a strong reference through the delegate. The WPF weak event pattern uses
WeakEventManager (including the generic WeakEventManager<TEventSource, TEventArgs>) so that the source does not keep
the listener alive. The MVVM Toolkit’s WeakReferenceMessenger addresses the same problem for messaging.
Keyboard input and focus
WPF distinguishes two kinds of focus, and mixing them up is the most frequent source of "focus does not move" bugs.
| Concept | API | Meaning |
|---|---|---|
Keyboard focus |
|
The one element that receives key events right now. Exactly one per application at a time. |
Logical focus |
|
The element that will receive keyboard focus when its focus scope becomes active. Each focus scope (a |
Focusability |
|
Which elements can be focused and in which order Tab visits them. |
When you focus a ToolBar button, keyboard focus moves to the button but the window’s logical focus stays on the
element that had it, so the focus returns when the toolbar command finishes. The key events are a tunnel and bubble
pair: PreviewKeyDown and KeyDown, PreviewKeyUp and KeyUp. Text arrives through PreviewTextInput and TextInput,
which is the correct place to filter characters because it is already composed by the input method editor (IME), unlike
KeyDown. KeyEventArgs.Key gives the logical key, SystemKey is used instead of Key when Alt is held, and
Keyboard.Modifiers returns the pressed modifiers. Key.ImeProcessed appears while an IME composes text.
private void OnWindowPreviewKeyDown(object sender, KeyEventArgs e)
{
// Ctrl+F focuses the search box from anywhere in the window.
if (e.Key == Key.F && Keyboard.Modifiers == ModifierKeys.Control)
{
Keyboard.Focus(SearchBox);
SearchBox.SelectAll();
e.Handled = true;
}
}
private void OnPriceTextInput(object sender, TextCompositionEventArgs e)
{
// Allow digits and at most one decimal separator; reject everything else before it reaches the TextBox.
var box = (TextBox)sender;
var kept = box.Text.Remove(box.SelectionStart, box.SelectionLength);
var separators = kept.Count(c => c == '.') + e.Text.Count(c => c == '.');
e.Handled = !e.Text.All(c => char.IsDigit(c) || c == '.') || separators > 1;
}
// Registered in the constructor with DataObject.AddPastingHandler(PriceBox, OnPricePasting).
private void OnPricePasting(object sender, DataObjectPastingEventArgs e)
{
// Pasted text does not raise PreviewTextInput, so validate it here.
if (e.DataObject.GetData(DataFormats.UnicodeText) is not string text
|| !decimal.TryParse(text, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out _))
{
e.CancelCommand();
}
}
Source: Focus overview (WPF)
Global shortcuts such as Ctrl+F are better expressed as KeyBinding or InputBinding entries in the
InputBindings of the Window, so that they show up as commands, work with RoutedUICommand text and can be disabled by CanExecute.
The code above shows the event path that those bindings are built on.
Mouse input
Mouse events follow the same tunnel and bubble pairs: PreviewMouseDown and MouseDown, PreviewMouseUp and MouseUp,
PreviewMouseMove and MouseMove, PreviewMouseWheel and MouseWheel, plus button-specific events such as
MouseLeftButtonDown. MouseEnter and MouseLeave are direct. MouseButtonEventArgs provides ChangedButton,
ButtonState and ClickCount, and e.GetPosition(element) returns the pointer position relative to any element,
including null for the screen-independent window coordinates.
Mouse capture redirects all mouse input to one element until released, even when the pointer leaves it. It is how
drags, sliders and splitters keep working. Call element.CaptureMouse() (or Mouse.Capture) on press, and release with
ReleaseMouseCapture() on release or when LostMouseCapture fires. The example resizes the cover preview of a book by
dragging its corner grip.
private Point _dragStart;
private double _startWidth;
private void OnGripMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton != MouseButton.Left)
{
return;
}
var grip = (UIElement)sender;
_dragStart = e.GetPosition(this);
_startWidth = CoverPreview.ActualWidth;
grip.CaptureMouse();
e.Handled = true;
}
private void OnGripMouseMove(object sender, MouseEventArgs e)
{
var grip = (UIElement)sender;
if (!grip.IsMouseCaptured)
{
return;
}
var delta = e.GetPosition(this).X - _dragStart.X;
CoverPreview.Width = Math.Max(80, _startWidth + delta);
}
private void OnGripMouseUp(object sender, MouseButtonEventArgs e) =>
((UIElement)sender).ReleaseMouseCapture();
Source: Input overview (WPF)
Mouse.OverrideCursor forces a cursor for the whole app (a wait cursor during a long load); it must be reset to null.
Because MouseWheel events bubble to the first ancestor that does not handle them, a ListBox inside a ScrollViewer
consumes the wheel and the outer viewer never scrolls, a known pitfall fixed by handling PreviewMouseWheel on the inner
control and re-raising the event on its parent.
Stylus, touch and manipulation
WPF models three physical devices on top of the same event system. Stylus events (StylusDown, StylusMove,
StylusUp, StylusInAirMove) carry pressure and tablet data and drive InkCanvas. Touch events (TouchDown,
TouchMove, TouchUp, TouchEnter, TouchLeave) deliver multi-touch contacts, each with a TouchDevice.Id.
Manipulation is a higher level that interprets one or more contacts as translation, scale and rotation, with optional
inertia, through ManipulationStarting, ManipulationStarted, ManipulationDelta, ManipulationInertiaStarting and
ManipulationCompleted. It is enabled by setting IsManipulationEnabled to true.
When a touch or stylus event is not handled, WPF promotes it to the corresponding mouse events so that mouse-only code
keeps working. If you handle TouchDown and set Handled, the mouse promotion stops. The default WPF input stack is the
original WPF stylus and touch stack; on Windows 8 and later an opt-in pointer stack based on the Windows
pointer messages can be enabled with an AppContext switch (Switch.System.Windows.Input.Stylus.EnablePointerSupport),
which changes how events are generated. The conceptual docs describe the touch events but do not describe the switch in
detail, so check the current reference before depending on it.
<Border x:Name="CoverPreview"
IsManipulationEnabled="True"
ManipulationStarting="OnCoverManipulationStarting"
ManipulationDelta="OnCoverManipulationDelta"
Width="160" Height="220">
<Border.RenderTransform>
<MatrixTransform />
</Border.RenderTransform>
<Image Source="{Binding SelectedBook.CoverUri}" Stretch="Uniform" />
</Border>
private void OnCoverManipulationStarting(object? sender, ManipulationStartingEventArgs e)
{
e.ManipulationContainer = this;
e.Mode = ManipulationModes.Scale | ManipulationModes.Translate;
}
private void OnCoverManipulationDelta(object? sender, ManipulationDeltaEventArgs e)
{
var cover = (FrameworkElement)e.Source;
var transform = (MatrixTransform)cover.RenderTransform;
var matrix = transform.Matrix;
var scale = e.DeltaManipulation.Scale;
matrix.ScaleAt(scale.X, scale.Y, e.ManipulationOrigin.X, e.ManipulationOrigin.Y);
matrix.Translate(e.DeltaManipulation.Translation.X, e.DeltaManipulation.Translation.Y);
transform.Matrix = matrix;
e.Handled = true;
}
ManipulationContainer defines the element whose coordinate system the deltas are expressed in, which is why it is set to
the window and not the moving element. Setting a MatrixTransform is the usual way to apply the resulting matrix without
changing layout.
WPF, WinUI 3 and Windows Forms
| Concept | WPF | WinUI 3 | Windows Forms |
|---|---|---|---|
Event model |
Routed events ( |
Routed events, bubbling only. |
Plain CLR events on each control. No routing; a handler is attached to the control that raises it. |
Preview events |
A |
Only a few preview events (for example |
|
|
|
|
|
Pointer input |
Separate mouse, stylus and touch events with promotion to mouse. |
Unified pointer events ( |
Mouse events ( |
Capture |
|
|
|
Focus |
Keyboard focus plus logical focus per focus scope. |
|
|
WinUI input is documented under the Windows App SDK input pages, and Windows Forms in the .NET desktop guide for Windows Forms.
Pitfalls
-
Handler never fires. A control handled the event first. Use the
Previewevent,AddHandlerwithhandledEventsToo: true, or a more specific event such asClick. -
Setting
Handledon a Preview event. It also suppresses the bubbling pair and all handlers that did not opt in to handled events, including those on ancestors and in other components that rely on the event. -
Using
Sourcefor the clicked child.Sourceis re-targeted by composite controls. UseOriginalSourcefor the real hit-tested element, and find the item withDataContextorItemsControl.ContainerFromElement. -
Subscribing a long-lived object to short-lived events (or the reverse). Unsubscribe in
Unloadedor use the weak event pattern. Routed event handlers added throughAddHandlerare also held strongly by the element. -
Heavy work in
MouseMoveorManipulationDelta. These fire at the input rate. Keep handlers cheap and avoid layout changes in a loop; apply render transforms as shown in the manipulation example. -
Forgetting capture release. A control that captures the mouse and misses
MouseUp(a window loses activation) stays captured; handleLostMouseCaptureas a cleanup point. -
Expecting
EventTriggerto run code. In WPF it only starts storyboards. -
Treating
KeyDownas text. It is not IME-composed text and ignores the keyboard layout. UseTextInput. -
Security. Input handlers can read what the user types (a password box, an ISBN scanner). Never log
e.Text,e.Keyor clipboard data from a handler in a production build, and keep diagnosticDebug.WriteLinecalls out of release code.
What Microsoft Learn does not provide
The WPF routed events and input pages are conceptual and reference quality. There is no end-to-end guidance on choosing
between a code-behind event handler and a command in an MVVM app: the Commands page and the MVVM Toolkit docs cover
both halves separately, and the choice made here (events for view-only behavior, commands for anything that touches a
view model) is a convention, not an official rule. The pointer stack opt-in and its interaction with
Manipulation* events has reference documentation only, with no tutorial. The official docs also do not provide a
performance guide for high-rate input.