WPF Bootcamp
Routed Events and Commands
WPF interaction is not just "control raises a CLR event." Events can route through the tree, and commands provide a higher-level interaction contract that decouples gesture from behavior.
These systems are why a button click, menu item, shortcut key, and toolbar command can all trigger the same action cleanly, without the view turning into a pile of duplicated handlers.
What you'll learn
- The difference between bubbling, tunneling, and direct routed events.
- Why commands are usually better than raw click handlers for application actions.
- How
ICommandand command sources cooperate in MVVM. - How to reason about input as something that moves through the tree instead of stopping at one control.
- Tunneling: starts at the root and moves toward the source.
- Bubbling: starts at the source and moves upward.
- Direct: handled only by the source element.
<Button Content="Save"
Command="{Binding SaveCommand}"
CommandParameter="{Binding SelectedDocument}" />
That one button can participate in routed input, query whether the command can execute, and invoke the same application action that might also be exposed from a menu or keyboard shortcut.
The mental model in one sentence
Routed events describe how input notifications travel through the tree, while commands describe what action the UI intends to perform regardless of which gesture triggered it.
Events answer "what happened?"
A key was pressed, a mouse button went down, a click occurred, focus moved, or selection changed.
Commands answer "what should we do?"
Save, Delete, Refresh, Open, Undo, and similar actions represent intent rather than a specific UI gesture.
Why routed events exist
In a complex visual tree, the element the user interacts with is often nested inside many parents that also care about the interaction. WPF solves this by letting some events route through the tree rather than stopping at the immediate source element.
<Border MouseDown="OnBorderMouseDown">
<Button Content="Save" Click="OnSaveClick" />
</Border>
The button may be the most obvious source element, but the border, parent panels, and higher-level containers may also want to observe or influence the interaction. Routed events give the framework a consistent way to model that.
The three routing strategies
Tunneling
Tunneling events travel from the root toward the source. They are often named with a Preview prefix, such as PreviewMouseDown or PreviewKeyDown.
Bubbling
Bubbling events start at the source and travel back upward through ancestors. This is useful when parents want to react to something a child did.
Direct
Direct events are raised and handled only by the source element. They still use WPF's event system, but they do not travel through the tree.
Use tunneling when
A parent needs an early chance to inspect or intercept input before the source element fully handles it.
Use bubbling when
You want higher-level containers to respond to child interactions without wiring every child manually.
Source element vs route participants
The source element is where the event originated. Route participants are all the ancestors and descendants involved in the event path, depending on routing strategy.
<Window>
<Grid>
<Button Content="Delete" />
</Grid>
</Window>
If the user clicks the button, the button is the source. But the grid and window can still observe the routed event as it bubbles upward, or during the preview/tunneling phase as it travels downward.
This is why WPF interaction often feels compositional. Containers can participate in input policy without tightly coupling themselves to every child instance.
Handled events and why parent behavior can disappear
A routed event can be marked as handled along the route. Once that happens, later listeners may not see it unless they explicitly opt in to handled events.
This is useful because controls can consume input that they own semantically. It is also a common source of confusion when a parent handler never fires.
Practical takeaway: if an expected routed event is not reaching a parent, check whether a child control is handling it first.
Why commands matter
A command models intent: Save, Open, Delete, Refresh. A control is only the trigger. This lets the same command be reused by a button, a menu item, a keyboard gesture, or a context menu.
public sealed class EditorViewModel
{
public ICommand SaveCommand { get; }
}
That property says the view model exposes an action called Save. The view decides how to trigger it. One view might use a button, another might use a toolbar, and both can still point to the same command.
Commands separate gesture from intent
A click is a gesture. A keyboard shortcut is a gesture. A menu selection is a gesture. Save is the intent. Commands let you model the intent once and bind many gestures to it.
<Button Content="Save" Command="{Binding SaveCommand}" />
<MenuItem Header="_Save" Command="{Binding SaveCommand}" />
That is cleaner than duplicating save logic in both a click handler and a menu handler. The view stays declarative, and the action logic stays centralized.
CanExecute is part of the interaction contract
Commands do not only execute. They also answer whether execution is currently allowed. This lets the UI reflect application state naturally.
public sealed class EditorViewModel
{
public ICommand SaveCommand { get; }
private bool CanSave()
{
return HasUnsavedChanges;
}
}
If CanExecute returns false, buttons and menu items bound to that command can disable themselves automatically. This keeps the view synchronized with action availability.
When to use a command instead of a raw event handler
Use a command when
The interaction represents an application action or user intent that might be triggered from multiple places.
Use an event handler when
The interaction is local, purely visual, or tightly coupled to a control-specific behavior that is not really an application command.
For example, toggling an animation on pointer enter is often just a UI event concern. Saving a document, deleting an item, or opening a screen is command territory.
Commands in MVVM
Commands fit naturally into MVVM because the view binds to command properties exposed by the view model.
<Button
Content="Delete"
Command="{Binding DeleteCommand}"
CommandParameter="{Binding SelectedItem}" />
The view does not need to know how deletion works. It only declares that the button should trigger the view model's delete intent, optionally passing a parameter.
This keeps interaction logic testable and decoupled from specific visual elements.
Command parameters and context
A command often needs context, such as the currently selected item, the current document, or the row the user interacted with. Command parameters carry that context.
<Button
Content="Archive"
Command="{Binding ArchiveCommand}"
CommandParameter="{Binding SelectedMessage}" />
The command still models one intent, but the parameter tells it what to operate on. This is much cleaner than hard-wiring every control to a dedicated method just to pass along one object.
A worked interaction walkthrough
Consider this simple setup:
<Grid PreviewKeyDown="OnPreviewKeyDown">
<Button
Content="Save"
Command="{Binding SaveCommand}" />
</Grid>
- The user presses Enter while the button has focus.
- A preview or tunneling input event can travel from the root toward the button.
- The button processes the input according to its control behavior.
- The button may raise higher-level routed events associated with activation or click behavior.
- The bound command is queried for
CanExecute. - If execution is allowed, the command runs the save action.
- Parents can still participate in earlier or later phases of routed input if they need to coordinate broader UI behavior.
That full pipeline is why WPF interaction is so composable. Input travels through the tree, but actions stay expressible at a higher semantic level.
Common mistakes
- Putting application logic directly in click handlers when the action should be a command.
- Ignoring event routing and accidentally handling the same input at multiple levels.
- Using commands for trivial one-off visual tweaks that belong in a control or behavior instead.
- Forgetting that a parent may be seeing a routed event from a child rather than an event it originated itself.
- Debugging a missing handler without checking whether the event was already marked handled.
InkkSlinger parity note: This repository exposes routed event and command infrastructure under `UI/Events` and `UI/Commanding`. The same architectural split applies: routed input helps UI composition, while commands keep view behavior declarative.
Next, learn binding, because commands become far more useful once the view can pull both data and actions from a view model.