| name | avalonia-events |
| description | Use when working with Avalonia routed events — defining custom RoutedEvent, handling bubbling or tunneling events, attaching handlers with AddHandler, RoutingStrategies, the OnApplyTemplate/PART_ pattern, or class-level static handlers via AddClassHandler. |
Avalonia Routed Events
Overview
Avalonia uses routed events that travel the logical tree. Three strategies:
- Bubble — source → root (default for most input events)
- Tunnel — root → source (
Preview prefix by convention)
- Direct — only raised on source element
All input events (PointerPressed, KeyDown, etc.) are routed.
Handling Events
XAML:
<Button Click="Button_Click"/>
<StackPanel PointerPressed="Panel_PointerPressed"/>
Code-behind (direct +=):
myButton.Click += Button_Click;
private void Button_Click(object? sender, RoutedEventArgs e) { }
AddHandler — explicit routing strategy, required for tunneling:
myPanel.AddHandler(Button.ClickEvent, OnAnyButtonClick, RoutingStrategies.Bubble);
myPanel.AddHandler(InputElement.PointerPressedEvent, OnPreviewPointerPressed,
RoutingStrategies.Tunnel);
private void OnAnyButtonClick(object? sender, RoutedEventArgs e)
{
Console.WriteLine($"Clicked: {e.Source}");
}
Remove handler:
myPanel.RemoveHandler(Button.ClickEvent, OnAnyButtonClick);
Stopping Propagation
private void OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
e.Handled = true;
}
}
e.Handled = true prevents subsequent handlers in the route from firing.
Defining Custom Routed Events
public class MyControl : Control
{
public static readonly RoutedEvent<RoutedEventArgs> ActivatedEvent =
RoutedEvent.Register<MyControl, RoutedEventArgs>(
name: "Activated",
routingStrategy: RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> Activated
{
add => AddHandler(ActivatedEvent, value);
remove => RemoveHandler(ActivatedEvent, value);
}
protected void RaiseActivated()
=> RaiseEvent(new RoutedEventArgs(ActivatedEvent, this));
}
Custom event args:
public class ValueChangedEventArgs : RoutedEventArgs
{
public double OldValue { get; }
public double NewValue { get; }
public ValueChangedEventArgs(RoutedEvent routedEvent, object source,
double oldValue, double newValue)
: base(routedEvent, source)
{
OldValue = oldValue;
NewValue = newValue;
}
}
public static readonly RoutedEvent<ValueChangedEventArgs> ValueChangedEvent =
RoutedEvent.Register<MyControl, ValueChangedEventArgs>(
"ValueChanged", RoutingStrategies.Bubble);
Tunneling Events (Preview)
Convention: tunneling events prefixed with Preview.
public static readonly RoutedEvent<PointerPressedEventArgs> PreviewPointerPressedEvent =
RoutedEvent.Register<InputElement, PointerPressedEventArgs>(
"PreviewPointerPressed",
RoutingStrategies.Tunnel);
Use pattern:
myParent.AddHandler(InputElement.PointerPressedEvent, Handler, RoutingStrategies.Tunnel);
RoutingStrategies Enum
| Value | Meaning |
|---|
Direct | Only raised on source element |
Bubble | Source → root |
Tunnel | Root → source |
All | Both tunnel and bubble (fire twice) |
OnApplyTemplate Pattern
Called when ControlTemplate is applied. Use to wire up named template parts:
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_button = e.NameScope.Get<Button>("PART_Button");
_button.Click += OnPartButtonClick;
_indicator = e.NameScope.Find<Border>("PART_Indicator");
}
private Button? _button;
private Border? _indicator;
Template XAML:
<ControlTemplate TargetType="local:MyControl">
<StackPanel>
<Border Name="PART_Indicator" Background="Gray"/>
<Button Name="PART_Button" Content="Click"/>
</StackPanel>
</ControlTemplate>
Convention: template part names prefixed with PART_.
Declare expected parts (documentation + tooling):
[TemplatePart("PART_Button", typeof(Button))]
[TemplatePart("PART_Indicator", typeof(Border))]
public class MyControl : TemplatedControl { }
Class Handlers (Static, All Instances)
static MyControl()
{
Button.ClickEvent.AddClassHandler<MyControl>((instance, e) =>
instance.OnChildButtonClicked(e));
InputElement.PointerPressedEvent.AddClassHandler<MyControl>(
(instance, e) => instance.OnPreviewPointerPressed(e),
RoutingStrategies.Tunnel);
}
private void OnChildButtonClicked(RoutedEventArgs e)
{
Console.WriteLine($"Button clicked in {this}");
}
Interaction Events (Non-Routed) vs Routed
Control.AttachedToVisualTree / DetachedFromVisualTree — not routed, fires on control directly:
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
}
Common Mistakes
+= vs AddHandler routing strategies — myPanel.PointerPressed += Handler only catches Bubble by default; use AddHandler with RoutingStrategies.Tunnel for preview intercept
- Tunnel events won't fire with default
Bubble strategy — must explicitly pass RoutingStrategies.Tunnel to AddHandler
new RoutedEventArgs(MyEvent) missing source — always pass this as source: new RoutedEventArgs(MyEvent, this)
- Forgetting
e.Handled = true in a handler that should consume the event — other handlers in the route still fire
OnApplyTemplate without cleaning up previous subscriptions — template can be re-applied; detach old handlers before attaching new ones
e.NameScope.Get<T> throws if part missing — use Find<T> for optional parts to avoid KeyNotFoundException