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

Bubble

Starts at the element that raised the event and walks up to the root.

Most input and action events: MouseDown, KeyDown, Click, TextChanged.

Tunnel

Starts at the root and walks down to the element that raised the event. By convention the name starts with Preview.

PreviewMouseDown, PreviewKeyDown, PreviewTextInput: let a parent see, validate or cancel input first.

Direct

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 EventTrigger.

MouseEnter, MouseLeave, Loaded when no routing is useful.

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.

sequenceDiagram participant W as MainWindow participant G as Grid (details pane) participant C as BookCard (Border) participant T as TextBlock (title, OriginalSource) Note over W,T: Tunnelling phase, PreviewMouseDown W->>G: PreviewMouseDown (Source is the TextBlock) G->>C: PreviewMouseDown C->>T: PreviewMouseDown Note over T: The source element handles it first Note over W,T: Bubbling phase, MouseDown T->>C: MouseDown C->>G: MouseDown G->>W: MouseDown Note over W: A handler anywhere can set Handled to stop both phases

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}");
}

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 Button

Handle Click (or bind Command).

Click is raised by the button’s own class handling.

See raw input on a control that handles it

Use the Preview event (PreviewMouseLeftButtonDown, PreviewKeyDown).

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

AddHandler(event, handler, handledEventsToo: true) in code.

XAML attributes cannot set handledEventsToo.

Stop a control from doing its default action

Set e.Handled = true in a Preview handler.

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

EventManager.RegisterClassHandler in a static constructor.

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>

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;
    }
}

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));
    }
}

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

Keyboard.Focus(element), Keyboard.FocusedElement, UIElement.IsKeyboardFocused, IsKeyboardFocusWithin

The one element that receives key events right now. Exactly one per application at a time.

Logical focus

FocusManager.GetFocusedElement(scope), FocusManager.SetFocusedElement, FocusManager.IsFocusScope

The element that will receive keyboard focus when its focus scope becomes active. Each focus scope (a Window, Menu, ToolBar) remembers its own.

Focusability

UIElement.Focusable, IsTabStop, TabIndex, KeyboardNavigation.TabNavigation

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();
    }
}

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();

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 (Bubble, Tunnel, Direct) with RoutedEvent objects.

Routed events, bubbling only. RoutedEventArgs.OriginalSource exists; there is no Tunnel strategy.

Plain CLR events on each control. No routing; a handler is attached to the control that raises it.

Preview events

A Preview* pair for every input event.

Only a few preview events (for example PreviewKeyDown); otherwise handle at the source or the parent.

Form.KeyPreview = true lets the form see key events before the focused control.

Handled

RoutedEventArgs.Handled; handlers skipped unless handledEventsToo.

Handled on the event args; UIElement.AddHandler(..., handledEventsToo) is available.

KeyEventArgs.Handled and SuppressKeyPress; no route to stop.

Pointer input

Separate mouse, stylus and touch events with promotion to mouse.

Unified pointer events (PointerPressed, PointerMoved, PointerReleased) plus Tapped and ManipulationDelta.

Mouse events (MouseDown, MouseMove), touch only through gestures and WndProc messages.

Capture

CaptureMouse() / Mouse.Capture.

CapturePointer(pointer) on the element.

Control.Capture = true.

Focus

Keyboard focus plus logical focus per focus scope.

FocusManager, UIElement.Focus(FocusState), XAML focus states.

Control.Focus(), ActiveControl, Enter and Leave events.

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 Preview event, AddHandler with handledEventsToo: true, or a more specific event such as Click.

  • Setting Handled on 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 Source for the clicked child. Source is re-targeted by composite controls. Use OriginalSource for the real hit-tested element, and find the item with DataContext or ItemsControl.ContainerFromElement.

  • Subscribing a long-lived object to short-lived events (or the reverse). Unsubscribe in Unloaded or use the weak event pattern. Routed event handlers added through AddHandler are also held strongly by the element.

  • Heavy work in MouseMove or ManipulationDelta. 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; handle LostMouseCapture as a cleanup point.

  • Expecting EventTrigger to run code. In WPF it only starts storyboards.

  • Treating KeyDown as text. It is not IME-composed text and ignores the keyboard layout. Use TextInput.

  • Security. Input handlers can read what the user types (a password box, an ISBN scanner). Never log e.Text, e.Key or clipboard data from a handler in a production build, and keep diagnostic Debug.WriteLine calls 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.