Skip to main content

Events

Avalonia's event system is conceptually similar to WPF's routed event model. Events can bubble up or tunnel down the visual tree, and you can register class handlers or instance handlers. However, there are important differences in the API surface, event naming, and how tunnelling is handled. This guide covers the key differences you need to know when migrating from WPF to Avalonia.

Routed events

Both WPF and Avalonia support routed events, but the registration API differs. In WPF, you use EventManager.RegisterRoutedEvent, while in Avalonia you use RoutedEvent.Register.

WPF
public static readonly RoutedEvent TapEvent = EventManager.RegisterRoutedEvent(
"Tap",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(MyControl));
Avalonia
public static readonly RoutedEvent<RoutedEventArgs> TapEvent = RoutedEvent.Register<MyControl, RoutedEventArgs>(
"Tap",
RoutingStrategy.Bubble);

Key differences to note:

  • Avalonia uses a generic RoutedEvent<TEventArgs> type, providing stronger typing for event arguments.
  • The registration call in Avalonia uses generic type parameters for both the owner type and the event args type, rather than passing typeof() arguments.
  • The delegate type is inferred from the generic type parameter in Avalonia, so you do not need to specify it explicitly.

Class handlers

In WPF, class handlers for events can be added by calling EventManager.RegisterClassHandler. In Avalonia, you call AddClassHandler directly on the routed event instance.

WPF
static MyControl()
{
EventManager.RegisterClassHandler(typeof(MyControl), MyEvent, HandleMyEvent));
}

private static void HandleMyEvent(object sender, RoutedEventArgs e)
{
}
Avalonia
static MyControl()
{
MyEvent.AddClassHandler<MyControl>((x, e) => x.HandleMyEvent(e));
}

private void HandleMyEvent(RoutedEventArgs e)
{
}

Notice that in WPF you have to add the class handler as a static method, whereas in Avalonia the class handler is not static: the notification is automatically directed to the correct instance. The sender parameter typical of event handlers is not necessary in this case and everything remains strongly typed.

Tunnelling events

In WPF, tunnelling (preview) events are exposed as separate CLR events with a Preview prefix. For example, PreviewKeyDown is the tunnelling counterpart to KeyDown. These are two distinct CLR events that you can subscribe to independently.

Avalonia takes a different approach. There are no separate Preview* CLR events. Instead, tunnelling and bubbling share the same RoutedEvent instance. To subscribe to the tunnelling phase, you call AddHandler and pass RoutingStrategies.Tunnel.

WPF
// In WPF, subscribe to the Preview event directly
myControl.PreviewKeyDown += OnPreviewKeyDown;

void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
// Tunnelling handler
}
Avalonia
// In Avalonia, use AddHandler with RoutingStrategies.Tunnel
myControl.AddHandler(InputElement.KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel);

void OnPreviewKeyDown(object? sender, KeyEventArgs e)
{
// Tunnelling handler
}

You can also subscribe to both tunnelling and bubbling phases simultaneously by combining the flags:

Avalonia
myControl.AddHandler(
InputElement.KeyDownEvent,
OnKeyDown,
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);

Event handler attachment

XAML event handlers

Attaching event handlers in XAML works the same way in both WPF and Avalonia:

<Button Click="OnButtonClick" />

Code-behind with AddHandler

In WPF, AddHandler takes the routed event and a delegate. Avalonia's AddHandler accepts additional parameters for routing strategy and handled-events behavior.

WPF
myButton.AddHandler(Button.ClickEvent, new RoutedEventHandler(OnButtonClick));
Avalonia
myButton.AddHandler(Button.ClickEvent, OnButtonClick);

The handledEventsToo parameter

Both WPF and Avalonia support receiving events even after they have been marked as handled. The parameter works similarly in both frameworks.

WPF
myControl.AddHandler(
UIElement.MouseDownEvent,
new MouseButtonEventHandler(OnMouseDown),
handledEventsToo: true);
Avalonia
myControl.AddHandler(
InputElement.PointerPressedEvent,
OnPointerPressed,
RoutingStrategies.Bubble,
handledEventsToo: true);

Note that in Avalonia you must specify the RoutingStrategies parameter before handledEventsToo.

Common event name differences

Many input events have different names in Avalonia compared to WPF. The following table lists the most common mappings:

WPF EventAvalonia EquivalentNotes
MouseLeftButtonDownPointerPressedCheck PointerUpdateKind for button type
MouseLeftButtonUpPointerReleasedCheck PointerUpdateKind for button type
MouseRightButtonDownPointerPressedCheck PointerUpdateKind for button type
MouseRightButtonUpPointerReleasedCheck PointerUpdateKind for button type
MouseMovePointerMoved
MouseEnterPointerEntered
MouseLeavePointerExited
MouseWheelPointerWheelChanged
PreviewKeyDownUse AddHandler with RoutingStrategies.Tunnel on KeyDownEventNo separate Preview event
PreviewKeyUpUse AddHandler with RoutingStrategies.Tunnel on KeyUpEventNo separate Preview event
PreviewMouseDownUse AddHandler with RoutingStrategies.Tunnel on PointerPressedEventNo separate Preview event

Avalonia uses pointer-based event names because it supports pointer devices beyond a mouse, including touch and pen input.

Custom routed events

When defining custom routed events, the registration pattern differs between WPF and Avalonia. Below is a complete comparison showing how to define, register, and raise a custom routed event.

WPF
public class MyControl : Control
{
public static readonly RoutedEvent TapEvent = EventManager.RegisterRoutedEvent(
"Tap",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(MyControl));

public event RoutedEventHandler Tap
{
add => AddHandler(TapEvent, value);
remove => RemoveHandler(TapEvent, value);
}

protected void OnTap()
{
RaiseEvent(new RoutedEventArgs(TapEvent));
}
}
Avalonia
public class MyControl : Control
{
public static readonly RoutedEvent<RoutedEventArgs> TapEvent = RoutedEvent.Register<MyControl, RoutedEventArgs>(
"Tap",
RoutingStrategy.Bubble);

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

protected void OnTap()
{
RaiseEvent(new RoutedEventArgs(TapEvent));
}
}

The main differences are:

  • Avalonia uses the generic RoutedEvent<T> for type safety.
  • The CLR event wrapper in Avalonia uses EventHandler<RoutedEventArgs> rather than RoutedEventHandler.
  • Registration uses generic type parameters instead of typeof() arguments.

See also