⚠ Deprecated: Legacy
NavigationFrame for Avalonia UI
License
—
Deps
6
Install Size
—
Vulns
✓ 0
Published
Dec 31, 2025
$ dotnet add package NavigationFrame.AvaloniaA modern, flexible, and source-generator-powered navigation framework for Avalonia applications. Inspired by Blazor and web navigation patterns, it brings strongly-typed routing, powerful layout systems, and automatic ViewModel wiring to your desktop apps.
navigator.NavigateAsync(new ProductRoute(123))).HomePage -> HomeViewModel). Supports 4 distinct lookup strategies.[Layout]
attribute.INavigatingFrom, IPreloadable, INavigatedTo, etc.) to handle
data loading, navigation guards, and cleanup.IsNavigating property with configurable ShowDelay and HideDelay to prevent
flickering for fast navigations.Microsoft.Extensions.DependencyInjection, including
support for Scoped Services per page.Register the core services in your App.axaml.cs:
using NavigationFrame.Avalonia;
using Microsoft.Extensions.DependencyInjection;
public void ConfigureServices(IServiceCollection services)
{
// 1. Register ViewFactory (Generated or Manual)
services.AddSingleton<IViewFactory, ViewFactory>(); // See step 2
// 2. Register NavigationService
services.AddSingleton<INavigationService, NavigationService>();
// or, scoped navigation service, treats a page without layout as a `Scope`
// and all the pages that use a same top-level layout as a `Scope`
services.AddSingleton<INavigationService, ScopedNavigationService>();
// 3. Register your ViewModels
services.AddTransient<HomeViewModel>();
services.AddTransient<MainLayoutViewModel>();
}
Create a partial class marked with [ViewFactory]. The source generator will implement the logic to resolve your views.
using NavigationFrame.Avalonia;
namespace MyApp.Services;
[ViewFactory]
public partial class ViewFactory : IViewFactory;
If you prefer to implement the factory manually, create a class that implements IViewFactory.
In your window, bind the NavigationService.Content to a ContentControl 's Content.
MainWindow.axaml:
<Window>
<Panel>
<!-- The main navigation content -->
<ContentControl Content="{Binding NavigationService.Content}" />
<!-- Global Progress Indicator -->
<ProgressBar IsIndeterminate="True"
IsVisible="{Binding NavigationService.IsNavigating}"
VerticalAlignment="Top"/>
</Panel>
</Window>
MainWindowViewModel.cs:
public class MainWindowViewModel : ViewModelBase
{
public INavigationService NavigationService { get; }
public MainWindowViewModel(INavigationService navService)
{
NavigationService = navService;
}
}
MainWindow.cs:
public class MainWindow : Window
{
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
if(DataContext is MainWindowViewModel { NavigationService: INavigationService navService })
{
// Setup default behaviors
navService.HandleBackwardInput = true; // Alt+Left or MouseBack
navService.HandleForwardInput = true; // Alt+Right or MouseForward
// Navigate to the start page
_ = navService.NavigateAsync(new LandingPageRoute());
}
}
}
Mark your Page's Route class (record recommended) with [Route<TPage>]. The framework will generate the implementation
of IRoute for it.
// Simple Route
[Route<HomePage>]
public partial record HomePageRoute;
// Route with Parameters
[Route<ProductPage>]
public partial record ProductPageRoute(int Id, string Category);
// Navigate:
await navigator.NavigateAsync(new ProductPageRoute(101, "Electronics"));
You can inherit from your own base Route class to add shared properties (e.g., for Menu generation).
public abstract record TabRoute
{
public abstract string TabName { get; }
public abstract string Icon { get; }
}
[Route<SettingsPage>]
public partial record SettingsPageRoute : TabRoute
{
public override string TabName => "Settings";
public override string Icon => "Gear";
}
[Route<HomePage>]
public partial record HomePageRoute : TabRoute
{
public override string TabName => "Home";
public override string Icon => "Home";
}
The INavigationService provides a comprehensive set of methods:
NavigateAsync(route, options): Pushes a new page onto the stack.GoBackAsync(): Pops the current page.GoForwardAsync(): Navigates to the next page (if any) in the stack.RefreshAsync(): Reloads the current page (triggers IPreloadable again).GoToAsync(route, options): Navigates to an existing page in the stack if found; otherwise pushes a new one.
MatchStrategy in NavOptions (ByValue, ByType, or ByReference).Post(mode, route, options): Safe way to trigger navigation from within a lifecycle callback (prevents deadlock).Layouts are wrapper controls. Mark them with [Layout] and implement IMountableControl.
[Layout]
public partial class MainLayout : UserControl, IMountableControl
{
// Return the placeholder where the page content should be placed
public Control GetMountPoint() => this.FindControl<ContentControl>("Body");
}
Apply a layout to a route:
[Route<HomePage, MainLayout>]
public partial record HomePageRoute;
Create a hierarchy by specifying a parent layout for your layout.
// AuthLayout is wrapped by MainLayout
[Layout<MainLayout>]
public partial class AuthLayout : UserControl, IMountableControl;
The framework infers the ViewModel type using 4 strategies in order:
Namespace.HomePage -> Namespace.HomeViewModelNamespace.Views.HomePage -> Namespace.ViewModels.HomeViewModelNamespace.HomePage -> Namespace.ViewModels.HomeViewModel (Appended .ViewModels)HomeViewModel in the assembly.If inference fails or you want a specific ViewModel:
[Route<MyPage, MyLayout>(DataContext = typeof(CustomViewModel))]
public partial record MyPageRoute;
If the page should use inherited DataContext:
// Turn off inference and "ViewModel not found" warning
[Route<MyPage, MyLayout>(InheritContext = true)]
public partial record MyPageRoute;
The framework provides a flexible authorization system that works at both the route level and the UI level.
Assign the Authorizer property on your INavigationService instance (usually in your MainViewModel or App startup).
// DI example
// register IAuthenticationStateProvider
// You need to implement AppAuthenticationStateProvider(IAuthenticationStateProvider)
services.AddSingleton<AppAuthenticationStateProvider>();
services.AddSingleton<IAuthenticationStateProvider>(sp => sp.GetRequiredService<AppAuthenticationStateProvider>());
// register IRoleAuthorizationPolicy, framework provides the DefaultRoleAuthorizationPolicy
services.AddSingleton<IRoleAuthorizationPolicy, DefaultRoleAuthorizationPolicy>();
// register IPolicyProvider, framework provides the DefaultPolicyProvider
services.AddSingleton<IPolicyProvider>(sp =>
{
var provider = new DefaultPolicyProvider();
provider.RegisterHandlers(
new CanEditDataPolicyHandler(),
new CanDeleteUserPolicyHandler(),
new CanExportDataPolicyHandler(),
);
return provider;
});
services.AddSingleton(new AuthorizationOptions
{
EnableCache = true,
CacheExpiration = TimeSpan.FromMinutes(5)
});
// register AuthorizationService, framework provides the AuthorizationService
services.AddSingleton<AuthorizationService>();
// Example in MainViewModel
public MainViewModel(INavigationService navService, AuthorizationService authService)
{
NavigationService = navService;
NavigationService.Authorizer = authService.CreateAuthorizer();
}
Add the [Authorize] attribute to your routes to protect pages:
[Route<AdminPage>]
[Authorize(Roles = "Admin", Policy = "RequireSuperUser")]
public partial record AdminPageRoute;
When navigating to this route, the NavigationService will call your authorization delegate. If it returns false,
navigation is cancelled.
Use Authorize attached properties to show/hide UI elements based on authorization. The Visible and Enable properties accept a flexible string format:
<UserControl xmlns:nav="using:NavigationFrame.Avalonia" nav:Authorize.Authorizer="{Binding Authorizer}">
<StackPanel>
<!-- Only check if authenticated -->
<TextBox nav:Authorize.Enable="" />
<!-- Only Policy (Positional) -->
<TextBox nav:Authorize.Enable="CanEditData" />
<!-- Policy + Roles (Positional) -->
<TextBox nav:Authorize.Enable="CanEditData; Admin,Manager" />
<!-- Explicit Keys -->
<TextBox nav:Authorize.Enable="Policy=CanEditData" />
<TextBox nav:Authorize.Enable="Roles=Admin,Manager" />
<!-- Mixed / Order Independent -->
<TextBox nav:Authorize.Enable="Policy=CanEditData; Roles=Admin" />
<!-- Short Keys -->
<TextBox nav:Authorize.Enable="P=CanEditData; R=Admin" />
</StackPanel>
</UserControl>
Tips:
Authorizer: Set this property on your INavigationService 's content holder control to enable global authorization. ** You only set once **. <!-- set once, effective on all nested controls -->
<ContentControl Content="{Binding NavigationService.Content}"
nav:Authorize.Authorizer="{Binding NavigationService.Authorizer}"/>
Access the Authorizer delegate via INavigationService to perform manual checks:
public class SettingsViewModel(INavigationService nav) : ViewModelBase
{
public async Task DeleteUserAsync()
{
if (nav.Authorizer != null)
{
bool canDelete = await nav.Authorizer(null, "Admin", "CanDelete");
if (!canDelete) return;
}
// ...
}
}
Implement these interfaces in your ViewModel or View to hook into events.
| Interface | Method | Description |
|---|---|---|
IRequireNavigator | SetNavigator(INavigator) | Receive the navigator instance. |
IPreloadable | PreloadAsync(route, mode, token) | Background Thread. Load data before the page is shown. Exception safe (captured in task). |
IRequireInit | InitializeAsync(token) | UI Thread. One-time initialization when the view is created. Blocks navigation until done. |
INavigatingFrom | OnNavigatingFromAsync() | Guard. Return false to cancel navigation (e.g., "Unsaved Changes"). |
INavigatedTo | OnNavigatedToAsync(route, preload, mode) | Called when page is active. Await the preload task here to handle data/errors. |
INavigatedFrom | OnNavigatedFromAsync() | Page is no longer active. Pause background tasks here. |
IBodyAware | OnBodyChangedAsync(route, body) | For Layouts. Called when the inner content changes (e.g., to update window title). |
IReleaseAware | OnReleased() | Called when the component is no longer referenced by the framework. |
public partial class ProductViewModel : ViewModelBase, IPreloadable, IRequireNavigator, INavigatingFrom
{
private INavigator _navigator;
public void SetNavigator(INavigator navigator)
{
_navigator = navigator;
}
private Product? _product;
public async Task PreloadAsync(IRoute route, NavigationMode mode,CancellationToken token)
{
// Load data here.
// This runs on a background thread. Dispatch to UI thread if needed.
if (route is ProductRoute productRoute)
{
// here we store the result in a field and use it until `OnNavigatedToAsync`
_product = await LoadProductAsync(productRoute.Id, token);
}
}
public async Task<bool> OnNavigatingFromAsync()
{
if (HasUnsavedChanges)
{
// Show confirmation dialog, return false to cancel navigation
return await _dialogService.ShowConfirmAsync("Discard changes?");
}
return true; // return true to allow navigating away
}
}
The framework never helps to invoke IDisposable.Dispose(), according to
the Microsoft documentation.
Who created the instance is responsible for disposing it. The generated IViewFactory implementation uses
IServiceProvider to create instances, which means it's up to DI container to manage the lifetime of the instance. If
you manually implement IViewFactory, you're responsible for disposing the created instances.
If your component implements IReleaseAware, the framework will call OnReleased() when the component is no longer
referenced by the framework. You can use this to release resources, such as closing database connections or disposing of
IDisposable resources. Tips: if your component implements IDisposable, and it's created by DI, disposing should be
done by DI to keep consistency.
Control animations and stack manipulation using NavOptions.
var options = new NavOptions
{
StackBehavior = StackBehaviors.Clear, // Clear history
Animation = PageAnimations.SlideLeft, // Built-in animation
IsEphemeral = true, // Skip adding to history
MatchStrategy = MatchStrategies.ByValue
};
await navigator.NavigateAsync(new HomePageRoute(), options);
Never await a navigation method inside a lifecycle callback (like OnNavigatingFromAsync). The navigation queue
is blocked waiting for your callback!
Incorrect:
public async Task OnNavigatedFromAsync() {
await _navigator.GoBackAsync(); // DEADLOCK
}
Correct:
public async Task OnNavigatedFromAsync() {
_navigator.Post(NavigationMode.GoBack, null); // Safe
}
Use IPreloadable for heavy lifting. It runs in parallel with the old page's exit animation. Await the result in
OnNavigatedToAsync.
public Task PreloadAsync(IRoute route, NavigationMode mode, CancellationToken t)
{
// Start loading, return the task.
// Do NOT await here if you want to block 'OnNavigatedTo' manually later.
return _service.LoadDataAsync(t);
}
public async Task OnNavigatedToAsync(IRoute route, Task preload, NavigationMode mode)
{
try
{
await preload; // Wait for data, catch exceptions here
UpdateUi();
}
catch (Exception ex)
{
ShowError(ex);
}
}
ScopedNavigationService rather than NavigationService.ScopedNavigationService as a singleton/transient service, depend on your MainViewModel.IViewFactory/ PageViewModel (s) / LayoutViewModel (s) as scoped/transient services.The library includes Roslyn analyzers to help you keep your code clean and safe:
| Rule ID | Severity | Description |
|---|---|---|
| NAV001 | Error | Page must be a Control: Classes marked with [Route] must inherit from Avalonia.Controls.Control. |
| NAV002 | Error | Layout must be a Control: Classes marked with [Layout] must inherit from Avalonia.Controls.Control. |
| NAV003 | Warning | ViewModel not found: The generator could not infer the ViewModel type. Specify it explicitly. |
| NAV004 | Info | ViewModel inferred: Informational message showing which ViewModel was automatically paired. |
| NAV005 | Info | Redundant AsTemplate: AsTemplate = true is the default; you can remove this argument. |
| NAV006 | Info | Redundant ViewModel: The explicitly specified ViewModel matches the inferred one; you can remove this argument. |
| NAV007 | Error | Deadlock in Callback: Awaiting navigation inside a lifecycle callback (e.g., OnNavigatingFromAsync) causes a deadlock. |
| NAV008 | Warning | Unsafe Re-entrancy: Fire-and-forget navigation inside a callback is unsafe without Dispatcher.UIThread.Post. |
| NAV009 | Warning | Discouraged Await: Awaiting navigation in OnNavigatedToAsync is safe but discouraged as it delays completion. |
| NAV010 | Error | Invalid BaseOn Type: The type specified in BaseOn must be an abstract record class that inherits from IRoute. |