Skip to main content

Events overview

Avalonia uses a routed event system similar to WPF. Routed events can travel through the element tree, allowing parent elements to handle events raised by their children. This is fundamental to how input, interaction, and control behavior work in Avalonia.

Event routing strategies

Every routed event has a routing strategy that determines how the event travels through the element tree:

StrategyDirectionDescription
BubbleChild to parentThe event fires on the source element first, then travels up through each parent until it reaches the root. This is the most common strategy.
TunnelParent to childThe event fires on the root element first, then travels down through the tree to the source element. Tunneling events are typically used for preview/interception scenarios.
DirectSource onlyThe event fires only on the source element. It does not travel through the tree.

Events can combine strategies. For example, many input events use both Tunnel | Bubble, which means the event first tunnels down from the root, then bubbles back up from the source.

Bubble example

When a user clicks a Button inside a StackPanel inside a Window:

Window          ← event arrives here last (bubble)
└─ StackPanel ← event arrives here second
└─ Button ← event starts here (source)

Tunnel example

A tunneling event for the same tree:

Window          ← event starts here first (tunnel)
└─ StackPanel ← event arrives here second
└─ Button ← event arrives here last (source)

Handling routed events

In XAML

Attach an event handler using the event name as an attribute:

<Button Click="OnButtonClick" Content="Click me" />
private void OnButtonClick(object? sender, RoutedEventArgs e)
{
// sender is the Button that was clicked
// e.Source is the original source of the event
}

In code

Use AddHandler and RemoveHandler:

myButton.AddHandler(Button.ClickEvent, OnButtonClick);

// Later, to unsubscribe:
myButton.RemoveHandler(Button.ClickEvent, OnButtonClick);

Handling bubbled events on a parent

Because events bubble up the tree, you can handle a child's event on a parent element:

<StackPanel Tapped="OnStackPanelTapped">
<Button Content="Button 1" />
<Button Content="Button 2" />
<Button Content="Button 3" />
</StackPanel>
private void OnStackPanelTapped(object? sender, TappedEventArgs e)
{
// sender is the StackPanel (where the handler is attached)
// e.Source is the specific Button that was tapped
if (e.Source is Button button)
{
Debug.WriteLine($"Tapped: {button.Content}");
}
}

Marking events as handled

Set e.Handled = true to stop an event from continuing to route:

private void OnButtonClick(object? sender, RoutedEventArgs e)
{
e.Handled = true; // Prevents parent handlers from receiving this event
}

If you need to receive events that have already been marked as handled, use the handledEventsToo parameter:

myPanel.AddHandler(Button.ClickEvent, OnButtonClick, RoutingStrategies.Bubble, handledEventsToo: true);

RoutedEventArgs properties

PropertyTypeDescription
Sourceobject?The element that originally raised the event.
HandledboolWhether the event has been handled. Set to true to stop routing.
RouteRoutingStrategiesThe current routing phase (Tunnel, Bubble, or Direct).
RoutedEventRoutedEventThe routed event being raised.

Registering custom routed events

Define a custom routed event in your control:

public class MyControl : Control
{
public static readonly RoutedEvent<RoutedEventArgs> ValueChangedEvent =
RoutedEvent.Register<MyControl, RoutedEventArgs>(
nameof(ValueChanged),
RoutingStrategies.Bubble);

public event EventHandler<RoutedEventArgs>? ValueChanged
{
add => AddHandler(ValueChangedEvent, value);
remove => RemoveHandler(ValueChangedEvent, value);
}

protected virtual void OnValueChanged()
{
RaiseEvent(new RoutedEventArgs(ValueChangedEvent));
}
}

Custom event args

For events that carry additional data, create a custom RoutedEventArgs subclass:

public class ValueChangedEventArgs : RoutedEventArgs
{
public ValueChangedEventArgs(RoutedEvent routedEvent, double oldValue, double newValue)
: base(routedEvent)
{
OldValue = oldValue;
NewValue = newValue;
}

public double OldValue { get; }
public double NewValue { get; }
}

Class handlers

Class handlers let you respond to events for all instances of a type, typically registered in a static constructor. Class handlers run before instance handlers.

public class MyControl : Control
{
static MyControl()
{
PointerPressedEvent.AddClassHandler<MyControl>((control, args) =>
{
control.OnPointerPressedInternal(args);
});
}

private void OnPointerPressedInternal(PointerPressedEventArgs args)
{
// Handle for all instances of MyControl
}
}

Class handlers are useful for control implementations that need to intercept input events before any instance-level handler can mark them as handled.

Next steps