xaml-csharp-development-skill-for-avalonia

Input System and Routed Events

Table of Contents

  1. Scope and APIs
  2. Input and Routing Flow
  3. Routed Event Metadata and Diagnostics
  4. Authoring Patterns
  5. Best Practices
  6. Troubleshooting

Scope and APIs

Primary APIs:

Important members:

Reference source files:

Input and Routing Flow

Runtime flow in app code:

  1. Platform sends raw input.
  2. Input is translated to Avalonia routed input events.
  3. Event route is built (Direct, Tunnel, Bubble).
  4. Class handlers run first, then instance handlers.
  5. Handled controls downstream processing.

Routing strategy quick guide:

Use Tunnel for interception, Bubble for normal control-level handling.

Routed Event Metadata and Diagnostics

Use routed-event metadata APIs when auditing event topology in larger control trees:

Example diagnostics pass:

using Avalonia.Interactivity;

foreach (var routedEvent in RoutedEventRegistry.Instance.GetAllRegistered())
{
    _logger.Debug(
        "Event={Name}, Owner={Owner}, Args={Args}, HasRaisedSubscribers={HasSubscribers}",
        routedEvent.Name,
        routedEvent.OwnerType.Name,
        routedEvent.EventArgsType.Name,
        routedEvent.HasRaisedSubscriptions);
}

You can instrument one routed event to inspect route activity:

using Avalonia.Interactivity;

IDisposable raisedSub = InputElement.PointerPressedEvent.Raised
    .Subscribe(tuple =>
    {
        (object sender, RoutedEventArgs args) = tuple;
        _ = sender;
        _ = args.Route;
    });

IDisposable finishedSub = InputElement.PointerPressedEvent.RouteFinished
    .Subscribe(args => _ = args.Handled);

CancelRoutedEventArgs is the canonical cancelable routed-event args type. Constructor forms:

Cancelable custom event pattern:

using Avalonia.Interactivity;

public class DocumentHost : Avalonia.Controls.Control
{
    public static readonly RoutedEvent<CancelRoutedEventArgs> BeforeCloseEvent =
        RoutedEvent.Register<DocumentHost, CancelRoutedEventArgs>(
            nameof(BeforeClose),
            RoutingStrategies.Bubble);

    public event EventHandler<CancelRoutedEventArgs>? BeforeClose
    {
        add => AddHandler(BeforeCloseEvent, value);
        remove => RemoveHandler(BeforeCloseEvent, value);
    }

    public bool RequestClose()
    {
        var args = new CancelRoutedEventArgs(BeforeCloseEvent, this);
        RaiseEvent(args);
        return !args.Cancel;
    }
}

Low-level route creation with EventRoute is uncommon in app code, but when used:

Authoring Patterns

Custom routed event

using Avalonia.Interactivity;

public class CommitPanel : Avalonia.Controls.Control
{
    public static readonly RoutedEvent<RoutedEventArgs> CommitRequestedEvent =
        RoutedEvent.Register<CommitPanel, RoutedEventArgs>(
            nameof(CommitRequested),
            RoutingStrategies.Bubble);

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

    protected void RaiseCommitRequested()
    {
        RaiseEvent(new RoutedEventArgs(CommitRequestedEvent));
    }
}

Intercept in tunnel and keep bubbling intact when needed

root.AddHandler(
    Avalonia.Input.InputElement.PointerPressedEvent,
    (sender, e) =>
    {
        if (ShouldBlockPointer(e))
            e.Handled = true;
    },
    RoutingStrategies.Tunnel,
    handledEventsToo: false);

Reactive subscription to routed events

using Avalonia.Interactivity;

IDisposable sub = myControl
    .GetObservable(Avalonia.Input.InputElement.KeyDownEvent)
    .Subscribe(e => HandleKey(e));

Pointer details

void OnPointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
{
    var point = e.GetCurrentPoint((Avalonia.Visual)e.Source!);
    if (point.Properties.IsLeftButtonPressed)
    {
        // Use point.Position and modifiers for deterministic handling.
    }
}

Best Practices

Troubleshooting

  1. Event never fires:
    • Wrong routing strategy or wrong routed event instance.
    • Control is not hit-testable (IsHitTestVisible = false) or disabled.
  2. Event fires but handler never runs:
    • Upstream handler set Handled = true.
    • Your handler was added without handledEventsToo where needed.
  3. Gesture events not appearing:
    • Gesture recognition may be skipped (PreventGestureRecognition).
    • Pointer lifecycle is incomplete (press without move/release path).
  4. Duplicate handling:
    • Same handler attached both as class handler and instance handler.
    • Both Tunnel and Bubble are subscribed without guards.

XAML-First and Code-Only Usage

Default mode:

XAML-first references:

XAML-first usage example:

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="MyApp.Views.EditorView"
             PointerPressed="OnPointerPressed"
             KeyDown="OnKeyDown">
  <UserControl.KeyBindings>
    <KeyBinding Gesture="Ctrl+Enter" Command="{Binding CommitCommand}" />
  </UserControl.KeyBindings>
</UserControl>

Code-only alternative (on request):

AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, RoutingStrategies.Bubble);
AddHandler(InputElement.KeyDownEvent, OnKeyDown, RoutingStrategies.Bubble);