Is your feature request related to a problem? Please describe.
Proposal: Reduce UI Coupling in Avalonia 12 Navigation and Input APIs
Summary
I would like to propose reducing the dependency on concrete UI instances in some Avalonia 12 APIs, especially Navigation and routed pointer input.
The main concern is that several application-level behaviors still require developers to work directly with Page or Control instances.
For example, navigation currently requires an instantiated page:
await Navigation.PushAsync(new SettingsPage());
And in some pointer input scenarios, especially with Button, receiving handled pointer events requires directly calling:
button.AddHandler(
InputElement.PointerPressedEvent,
OnPointerPressed,
RoutingStrategies.Bubble,
handledEventsToo: true);
These APIs work, but both expose the same architectural problem:
Higher-level application behavior is unnecessarily coupled to concrete UI objects.
I think Avalonia would benefit from stronger framework-level abstractions where controls are the result of resolution/rendering, rather than something application logic must directly provide or manipulate.
1. Navigation should not require a Page instance
Currently, navigation is fundamentally based on passing an already-created Page:
await Navigation.PushAsync(new SettingsPage());
Conceptually, this means:
I believe this abstraction is too UI-centric.
A navigation request should describe where the application wants to navigate, not require the caller to construct the visual object first.
For example:
await Navigation.PushAsync(typeof(SettingsPage));
or preferably:
await Navigation.PushAsync<SettingsPage>();
The framework could then resolve or construct the page internally.
Why this matters
With the current design:
var page = new SettingsPage();
await Navigation.PushAsync(page);
the caller is responsible for both:
- Creating the UI
- Performing the navigation
In applications using dependency injection, it may become:
var page = serviceProvider.GetRequiredService<SettingsPage>();
await Navigation.PushAsync(page);
This introduces coupling between:
- Navigation
Page
- Dependency injection
- View lifetime
- ViewModel lifetime
- Application architecture
The navigation layer should not need the caller to resolve a visual object before expressing navigation intent.
A cleaner dependency direction would be:
Application
↓
Navigation request
↓
Page resolver / factory
↓
Page
rather than:
Application
↓
Page creation
↓
Navigation
Suggested navigation API
A minimal API could be:
Task PushAsync(Type pageType);
Task ReplaceAsync(Type pageType);
with generic overloads:
Task PushAsync<TPage>() where TPage : Page;
Task ReplaceAsync<TPage>() where TPage : Page;
Page creation could be delegated to a framework abstraction:
public interface INavigationPageResolver
{
Page Resolve(Type pageType);
}
The default implementation could use Activator.CreateInstance, while applications could provide their own resolver.
For example, a DI-based resolver could be:
public sealed class ServiceProviderPageResolver : INavigationPageResolver
{
private readonly IServiceProvider _services;
public ServiceProviderPageResolver(IServiceProvider services)
{
_services = services;
}
public Page Resolve(Type pageType)
{
return (Page)_services.GetRequiredService(pageType);
}
}
This would allow navigation to remain independent of page construction.
The existing API:
could remain available as a lower-level overload for cases where passing a specific instance is intentional.
Why Type is better than Page as the navigation input
Using Type instead of an instantiated Page provides several benefits.
Decoupled page construction
The caller only expresses the destination:
await Navigation.PushAsync<SettingsPage>();
It does not need to know how the page is created.
Better DI integration
The navigation framework can delegate resolution to an application-provided resolver without requiring business or ViewModel code to behave like a service locator.
Better lifetime control
The navigation system can decide when pages should be:
- Created
- Reused
- Cached
- Recreated
- Released
When the caller passes an existing instance, part of that lifetime decision has already been made outside the navigation framework.
Better testability
A navigation request can be represented using metadata rather than requiring a real control tree:
await navigation.PushAsync<SettingsPage>();
Assert.Equal(typeof(SettingsPage), navigation.CurrentPageType);
Better foundation for future abstractions
Once navigation is no longer fundamentally based on page instances, Avalonia could later support higher-level concepts such as:
NavigateAsync<SettingsViewModel>();
or:
NavigateAsync("settings");
without redesigning the entire navigation model.
2. Pointer input has a similar UI coupling problem
I encountered a similar issue when using pointer events on Button.
Avalonia exposes useful low-level events such as:
PointerPressed
PointerReleased
These are important when an application needs to distinguish between:
- The moment the left mouse button is pressed
- The moment the left mouse button is released
This is not equivalent to Click.
For example, an application may need to begin an operation on press and finish it on release.
That is why I used PointerPressed and PointerReleased instead of Click.
However, on Button, these events can already be handled internally by the control.
As a result, a normal routed event subscription may not receive them.
To receive the event, the application has to use:
button.AddHandler(
InputElement.PointerPressedEvent,
OnPointerPressed,
RoutingStrategies.Bubble,
handledEventsToo: true);
and similarly for PointerReleased.
This was surprising.
Avalonia exposes PointerPressed and PointerReleased, but using them on one of the most common controls may require directly accessing the Button instance and manually registering a handler from C#.
The problem is not that Button handles pointer events
It is completely reasonable for Button to internally handle pointer input.
A button needs to implement behavior such as:
PointerPressed
↓
Pressed state
↓
Pointer capture
↓
PointerReleased
↓
Click
It also needs to unify interaction from:
- Mouse
- Touch
- Pen
- Keyboard
- Accessibility input
So internally marking pointer events as handled is understandable.
The problem is what happens after that.
If application code still needs to observe the press/release phases, there should ideally be a convenient declarative or MVVM-friendly mechanism for receiving handled routed events.
Instead, the application currently has to return to:
with:
That forces application interaction logic back onto a concrete UI instance.
MVVM impact
Without this issue, the expected architecture would be:
Pointer interaction
↓
Command / Binding
↓
ViewModel
But when AddHandler is required, it becomes:
Pointer interaction
↓
Control instance
↓
AddHandler
↓
Event callback
↓
Command / ViewModel
The application now needs an additional View-specific bridge.
If a project wants to preserve MVVM, developers may have to implement custom infrastructure such as:
PointerPressedCommand
PointerReleasedCommand
using attached properties or behaviors.
Internally those abstractions still need to call:
AddHandler(..., handledEventsToo: true);
For larger projects, this can eventually lead to developers writing repetitive event-to-command infrastructure or even source generators simply to expose framework input events cleanly to ViewModels.
I do not think application developers should need to rebuild this layer themselves.
A possible input abstraction
Avalonia could provide a declarative mechanism for routed events that supports receiving already-handled events.
Conceptually, something like:
<Button>
<Button.PointerPressed>
<EventBinding
Command="{Binding PressCommand}"
HandledEventsToo="True" />
</Button.PointerPressed>
</Button>
or an equivalent behavior/attached API.
The exact syntax is less important than the architectural goal:
Receiving handled routed events should not require application code to manually obtain the Control instance and call AddHandler.
This would preserve Avalonia's routed event implementation while providing a cleaner MVVM-facing abstraction.
The common architectural issue
Navigation and pointer input may look unrelated, but I believe they expose the same underlying design problem.
Navigation currently becomes:
Application intent
↓
Page instance
↓
Navigation
Pointer interaction can become:
Application interaction
↓
Control instance
↓
AddHandler
In both cases, higher-level application behavior eventually depends directly on a concrete UI object.
This makes the Visual Tree more central to application architecture than necessary.
For a modern application architecture using DI and MVVM, I would expect something closer to:
Application / ViewModel
↓
Framework abstraction
↓
UI resolution / adapter
↓
Control
The concrete UI object should ideally exist at the edge of the architecture.
Why this matters especially for DI-based applications
A DI-oriented application usually wants dependencies and lifetimes to remain explicit:
IServiceProvider
├── Application services
├── Navigation
├── State
├── ViewModels
└── UI
The UI layer should consume application services.
However, APIs that require Page, Control, TopLevel, or other concrete visual objects can reverse that dependency:
Application service
↓
Needs UI object
↓
Needs Visual Tree / UI lifetime
This makes it harder to keep:
- ViewModels testable
- Services independent from the UI
- Views transient
- UI lifetime separate from application lifetime
- Navigation state independent from rendered controls
This becomes increasingly important for applications that have:
- Background services
- Tray functionality
- Multiple windows
- Long-running tasks
- IPC
- Networking
- State persistence
- UI recreation
In those applications, the UI is not necessarily the application itself. It is one frontend of the application.
Closing thoughts
Avalonia already provides strong support for:
- Data binding
- MVVM
- DataTemplates
- DI-based application architectures
- Cross-platform UI
For that reason, I think it would be valuable for framework APIs to continue moving away from requiring concrete UI instances for application-level behavior.
For navigation:
A Page should be the result of navigation resolution, not the input of a navigation request.
For pointer input:
Receiving a handled routed event should not require application code to manually access the control instance and call AddHandler.
More broadly:
Application behavior should depend on framework-level abstractions, while concrete controls should remain at the UI boundary.
Type-based navigation combined with an extensible page resolver, together with a declarative way to observe handled routed events, would significantly improve the experience of building DI- and MVVM-oriented Avalonia applications.
Describe the solution you'd like
My overall impression is that Avalonia is not particularly well suited to applications that rely heavily on dependency injection and strict separation between application architecture and the UI layer.
In several areas, I feel that the abstraction level of the framework APIs is too low. Many APIs expose or require direct interaction with concrete controls. Of course, direct control manipulation is powerful and flexible, and it can solve many problems, but I do not think it should be the primary abstraction for common application-level scenarios.
For me, the main concern is not whether these APIs are capable enough. They clearly are. The concern is that application architecture is often forced to depend on UI objects earlier than necessary.
Comparison with other frameworks
My main focus is Windows desktop development. The frameworks I use most frequently are WPF and WinUI.
WPF is an older framework with many APIs that reflect the design philosophy of its time, while WinUI represents a more modern API design in many areas. Despite their differences, I can usually switch between them without significantly changing the overall architecture of my application.
With Avalonia, however, I frequently find myself having to adapt the architecture around the UI framework itself.
For example, instead of keeping navigation, interaction logic, and application services independent from concrete controls, I often end up needing access to objects such as:
Page
Control
TopLevel
Window
This makes the UI layer feel more central to the application architecture than I would expect.
My preferred architecture is closer to:
Application / Services
↓
ViewModels / State
↓
UI abstraction
↓
Views / Controls
rather than:
Application logic
↓
Concrete Control / Page
↓
Framework API
This distinction is particularly important in applications that make extensive use of dependency injection, background services, multiple windows, persistent application state, or independently recreatable views.
I am not suggesting that direct control-based APIs should be removed. They are useful as low-level escape hatches.
However, I would prefer Avalonia to provide higher-level framework abstractions for common scenarios, while keeping direct control manipulation as the lower-level option.
Describe alternatives you've considered
The above description of the problem has been revised by GPT. We hope this version can make it easier for you to understand the issue.
Additional context
No response
Is your feature request related to a problem? Please describe.
Proposal: Reduce UI Coupling in Avalonia 12 Navigation and Input APIs
Summary
I would like to propose reducing the dependency on concrete UI instances in some Avalonia 12 APIs, especially Navigation and routed pointer input.
The main concern is that several application-level behaviors still require developers to work directly with
PageorControlinstances.For example, navigation currently requires an instantiated page:
And in some pointer input scenarios, especially with
Button, receiving handled pointer events requires directly calling:These APIs work, but both expose the same architectural problem:
I think Avalonia would benefit from stronger framework-level abstractions where controls are the result of resolution/rendering, rather than something application logic must directly provide or manipulate.
1. Navigation should not require a
PageinstanceCurrently, navigation is fundamentally based on passing an already-created
Page:Conceptually, this means:
I believe this abstraction is too UI-centric.
A navigation request should describe where the application wants to navigate, not require the caller to construct the visual object first.
For example:
or preferably:
The framework could then resolve or construct the page internally.
Why this matters
With the current design:
the caller is responsible for both:
In applications using dependency injection, it may become:
This introduces coupling between:
PageThe navigation layer should not need the caller to resolve a visual object before expressing navigation intent.
A cleaner dependency direction would be:
rather than:
Suggested navigation API
A minimal API could be:
with generic overloads:
Page creation could be delegated to a framework abstraction:
The default implementation could use
Activator.CreateInstance, while applications could provide their own resolver.For example, a DI-based resolver could be:
This would allow navigation to remain independent of page construction.
The existing API:
could remain available as a lower-level overload for cases where passing a specific instance is intentional.
Why
Typeis better thanPageas the navigation inputUsing
Typeinstead of an instantiatedPageprovides several benefits.Decoupled page construction
The caller only expresses the destination:
It does not need to know how the page is created.
Better DI integration
The navigation framework can delegate resolution to an application-provided resolver without requiring business or ViewModel code to behave like a service locator.
Better lifetime control
The navigation system can decide when pages should be:
When the caller passes an existing instance, part of that lifetime decision has already been made outside the navigation framework.
Better testability
A navigation request can be represented using metadata rather than requiring a real control tree:
Better foundation for future abstractions
Once navigation is no longer fundamentally based on page instances, Avalonia could later support higher-level concepts such as:
or:
without redesigning the entire navigation model.
2. Pointer input has a similar UI coupling problem
I encountered a similar issue when using pointer events on
Button.Avalonia exposes useful low-level events such as:
These are important when an application needs to distinguish between:
This is not equivalent to
Click.For example, an application may need to begin an operation on press and finish it on release.
That is why I used
PointerPressedandPointerReleasedinstead ofClick.However, on
Button, these events can already be handled internally by the control.As a result, a normal routed event subscription may not receive them.
To receive the event, the application has to use:
and similarly for
PointerReleased.This was surprising.
Avalonia exposes
PointerPressedandPointerReleased, but using them on one of the most common controls may require directly accessing theButtoninstance and manually registering a handler from C#.The problem is not that
Buttonhandles pointer eventsIt is completely reasonable for
Buttonto internally handle pointer input.A button needs to implement behavior such as:
It also needs to unify interaction from:
So internally marking pointer events as handled is understandable.
The problem is what happens after that.
If application code still needs to observe the press/release phases, there should ideally be a convenient declarative or MVVM-friendly mechanism for receiving handled routed events.
Instead, the application currently has to return to:
with:
That forces application interaction logic back onto a concrete UI instance.
MVVM impact
Without this issue, the expected architecture would be:
But when
AddHandleris required, it becomes:The application now needs an additional View-specific bridge.
If a project wants to preserve MVVM, developers may have to implement custom infrastructure such as:
using attached properties or behaviors.
Internally those abstractions still need to call:
For larger projects, this can eventually lead to developers writing repetitive event-to-command infrastructure or even source generators simply to expose framework input events cleanly to ViewModels.
I do not think application developers should need to rebuild this layer themselves.
A possible input abstraction
Avalonia could provide a declarative mechanism for routed events that supports receiving already-handled events.
Conceptually, something like:
or an equivalent behavior/attached API.
The exact syntax is less important than the architectural goal:
This would preserve Avalonia's routed event implementation while providing a cleaner MVVM-facing abstraction.
The common architectural issue
Navigation and pointer input may look unrelated, but I believe they expose the same underlying design problem.
Navigation currently becomes:
Pointer interaction can become:
In both cases, higher-level application behavior eventually depends directly on a concrete UI object.
This makes the Visual Tree more central to application architecture than necessary.
For a modern application architecture using DI and MVVM, I would expect something closer to:
The concrete UI object should ideally exist at the edge of the architecture.
Why this matters especially for DI-based applications
A DI-oriented application usually wants dependencies and lifetimes to remain explicit:
The UI layer should consume application services.
However, APIs that require
Page,Control,TopLevel, or other concrete visual objects can reverse that dependency:This makes it harder to keep:
This becomes increasingly important for applications that have:
In those applications, the UI is not necessarily the application itself. It is one frontend of the application.
Closing thoughts
Avalonia already provides strong support for:
For that reason, I think it would be valuable for framework APIs to continue moving away from requiring concrete UI instances for application-level behavior.
For navigation:
For pointer input:
More broadly:
Type-based navigation combined with an extensible page resolver, together with a declarative way to observe handled routed events, would significantly improve the experience of building DI- and MVVM-oriented Avalonia applications.
Describe the solution you'd like
My overall impression is that Avalonia is not particularly well suited to applications that rely heavily on dependency injection and strict separation between application architecture and the UI layer.
In several areas, I feel that the abstraction level of the framework APIs is too low. Many APIs expose or require direct interaction with concrete controls. Of course, direct control manipulation is powerful and flexible, and it can solve many problems, but I do not think it should be the primary abstraction for common application-level scenarios.
For me, the main concern is not whether these APIs are capable enough. They clearly are. The concern is that application architecture is often forced to depend on UI objects earlier than necessary.
Comparison with other frameworks
My main focus is Windows desktop development. The frameworks I use most frequently are WPF and WinUI.
WPF is an older framework with many APIs that reflect the design philosophy of its time, while WinUI represents a more modern API design in many areas. Despite their differences, I can usually switch between them without significantly changing the overall architecture of my application.
With Avalonia, however, I frequently find myself having to adapt the architecture around the UI framework itself.
For example, instead of keeping navigation, interaction logic, and application services independent from concrete controls, I often end up needing access to objects such as:
This makes the UI layer feel more central to the application architecture than I would expect.
My preferred architecture is closer to:
rather than:
This distinction is particularly important in applications that make extensive use of dependency injection, background services, multiple windows, persistent application state, or independently recreatable views.
I am not suggesting that direct control-based APIs should be removed. They are useful as low-level escape hatches.
However, I would prefer Avalonia to provide higher-level framework abstractions for common scenarios, while keeping direct control manipulation as the lower-level option.
Describe alternatives you've considered
The above description of the problem has been revised by GPT. We hope this version can make it easier for you to understand the issue.
Additional context
No response