NavigationFrame.Avalonia
2.0.3
See the version list below for details.
dotnet add package NavigationFrame.Avalonia --version 2.0.3
NuGet\Install-Package NavigationFrame.Avalonia -Version 2.0.3
<PackageReference Include="NavigationFrame.Avalonia" Version="2.0.3" />
<PackageVersion Include="NavigationFrame.Avalonia" Version="2.0.3" />
<PackageReference Include="NavigationFrame.Avalonia" />
paket add NavigationFrame.Avalonia --version 2.0.3
#r "nuget: NavigationFrame.Avalonia, 2.0.3"
#:package NavigationFrame.Avalonia@2.0.3
#addin nuget:?package=NavigationFrame.Avalonia&version=2.0.3
#tool nuget:?package=NavigationFrame.Avalonia&version=2.0.3
NavigationFrame.Avalonia
A 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.
✨ Key Features
- Source Generator Powered: Zero runtime reflection for route discovery. Routes are generated at compile time for maximum performance and type safety.
- Strongly Typed Navigation: Say goodbye to magic strings. Navigate using objects (e.g.,
navigator.NavigateAsync(new ProductRoute(123))). - Smart ViewModel Inference: Automatically locates and wires up ViewModels based on naming conventions (e.g.,
HomePage→HomeViewModel). - Advanced Layout System: Support for nested layouts (Master → Auth → Page) using the
[Layout]attribute. - Lifecycle Management: Rich lifecycle interfaces (
INavigatingFrom,IDataPreloader,INavigatedTo, etc.) to handle data loading, navigation guards, and cleanup. - Built-in Transitions: Easy-to-use animations (Slide, Fade, Zoom) configurable globally or per-route.
- Dependency Injection: Seamless integration with
Microsoft.Extensions.DependencyInjection. - Developer Experience: Includes compile-time diagnostics to catch errors early (e.g., missing ViewModels, redundant configurations).
🚀 Getting Started
1. Setup Dependency Injection
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>();
// 3. Register your ViewModels
services.AddTransient<HomeViewModel>();
services.AddTransient<MainLayoutViewModel>();
}
2. Implement ViewFactory
Generated ViewFactory
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;
Manual ViewFactory
If you prefer to implement the factory manually, create a class that implements IViewFactory.
3. Initialize Navigation Host
In your window, bind the NavigationService.Content to a ContentControl 's Content.
MainWindow.axaml:
<Window>
<ContentControl Content="{Binding NavigationService.Content}" />
</Window>
In your ViewModel, expose NavigationService property.
MainWindowViewModel.cs:
public class MainWindowViewModel : ViewModelBase
{
public INavigationService NavigationService { get; }
public MainWindowViewModel(INavigationService navService)
{
NavigationService = navService;
}
}
📖 Usage Guide
Defining Pages (Manual)
Implement IRoute to create a strongly-typed route for each page.
Defining Pages (Generated)
Mark your Page's Route class (record recommended) with [Route<TPage>]. The framework will generate the implementation of IRoute for it.
[Route<HomePage>]
public partial record HomePageRoute;
Passing Parameters
Just define parameters via properties.
[Route<ProductPage>]
public partial record ProductPageRoute(int Id, string Category);
// Navigate using the generated route:
await navigator.NavigateAsync(new ProductPageRoute(101, "Electronics"));
Custom Base Route
You are free to inherit from your Route base class.
[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";
}
Navigation Methods
NavigateAsync(route, options): Pushes a new page onto the stack.GoBackAsync(): Pops the current page.GoToAsync(route, options): Navigates to an existing page in the stack if found; otherwise pushes a new one.- Important: This method uses
IEquatable.Equalsto find the existing page. Since generated routes arerecordtypes, value-based equality is handled automatically.
- Important: This method uses
Layouts
Layouts are wrapper controls. Mark them with [Layout] and implement IMountableControl.
[Layout]
public partial class MainLayout : UserControl, IMountableControl
{
// Return the placeholder where you reserved for body
public Control GetMountPoint() => this.FindControl<ContentControl>("Body");
}
Apply a layout to a route using the second generic argument:
[Route<HomePage, MainLayout>]
public partial record HomePageRoute;
Nested Layouts
Layouts can have parent layouts, creating a hierarchy. Use LayoutAttribute<TLayout> on the layout class.
[Layout<MainLayout>]
public partial class AuthLayout : UserControl, IMountableControl;
ViewModel Wiring
The framework attempts to infer the ViewModel type based on conventions:
Namespace.HomePage→Namespace.HomeViewModelNamespace.Views.HomePage→Namespace.ViewModels.HomeViewModel
If your ViewModel doesn't follow these patterns, specify it explicitly using the third generic argument:
[Route<MyPage, MyLayout>(DataContext = typeof(CustomViewModel))]
public partial record MyPageRoute;
Diagnostics: The compiler will warn you if:
- ViewModel cannot be inferred and is not specified.
- You specify a ViewModel that is identical to the inferred one (Redundant).
- You can specify
InheritContext = trueto skip inference and warning.
Navigation Lifecycle & Interfaces
The framework provides granular interfaces to hook into navigation events. You can implement any combination of these interfaces in your ViewModel or View.
1. Obtaining Navigator (IRequireNavigator)
Implement IRequireNavigator to receive the INavigator instance.
public partial class HomeViewModel : ViewModelBase, IRequireNavigator
{
private INavigator _navigator;
public void SetNavigator(INavigator navigator)
{
_navigator = navigator;
}
}
2. Data Loading (IPreloadable)
Implement IPreloadable to preload data when the route changes. This is called on a background thread.
public partial class HomeViewModel : ViewModelBase, IPreloadable
{
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)
{
await LoadProductAsync(productRoute.Id, token);
}
}
}
3. Initialization (IRequireInit)
Implement IRequireInit for logic that must run on the UI thread once when the component is first created.
public partial class HomeViewModel : ViewModelBase, IRequireInit
{
public async Task InitializeAsync(CancellationToken token)
{
// UI-thread initialization
await InitializeMyControlsAsync();
}
}
4. Navigation Guard (INavigatingFrom)
Implement INavigatingFrom to intercept and potentially cancel navigation away from the current page.
public partial class HomeViewModel : ViewModelBase, INavigatingFrom
{
public async Task<bool> OnNavigatingFromAsync()
{
if (HasUnsavedChanges)
{
// Show confirmation dialog, return false to cancel navigation
return await _dialogService.ShowConfirmAsync("Discard changes?");
}
return true;
}
}
5. Navigation Notifications
INavigatedTo: Called when navigation to the page is complete.INavigatedFrom: Called after navigation away from the page is complete.
public partial class HomeViewModel : ViewModelBase, INavigatedTo, INavigatedFrom
{
public async Task OnNavigatedToAsync(IRoute route, Task preload, NavigationModemode)
{
// Page is now active
// You can await 'preload' to ensure data loading is finished
await preload;
// if you don't use 'preload', it's time to load data related to the route
}
public async Task OnNavigatedFromAsync()
{
// Page is no longer active, but may still be in the history stack.
// Suitable for pausing background tasks or resource suspension.
// For permanent cleanup, implement IDisposable.
}
}
6. Layout Body Changes (IBodyAware)
For Layouts, implement IBodyAware to be notified when the inner content (body) changes.
public partial class MainLayoutViewModel : ViewModelBase, IBodyAware
{
public async Task OnBodyChangedAsync(IRoute route, object? body)
{
// Handle body change (e.g., update title based on route)
_suppressNav = true; // block navigation while updating tab
if(route is TabRoute tabRoute)
{
// Update title based on tab route
var any = Tabs.FirstOrDefault(x => x.TabName == tabRoute.TabName);
if(any != null)
{
SelectedTab = any;
}
}
_suppressNav = false;
}
}
Transitions and Stack Behavior
Control animations using NavOptions.
Per-Navigation:
var route = new HomePageRoute();
var options = new NavOptions
{
StackBehavior = StackBehaviors.Clear, // clear all other pages
Animation = PageAnimations.CrossFade(),
IsEphemeral = true, // don't add to history stack
};
navigator.NavigateAsync(route, options);
💡 Best Practices & Common Pitfalls
Re-entrant Navigation (Redirecting)
Navigation methods (NavigateAsync, GoBackAsync, etc.) are queued and executed sequentially.
If you await a navigation method inside a lifecycle callback (e.g., OnNavigatingFromAsync, OnNavigatedFromAsync, OnBodyChangedAsync), you will cause a deadlock.
The queue is waiting for your callback to finish, but your callback is waiting for the queue to process the new navigation.
Rule: Only OnNavigatedToAsync is safe to await navigation methods, but not recommended.
Solution: If you need to navigate from other callbacks, use the Navigator.Post method or Dispatcher.UIThread.Post to break the re-entrancy:
// Safe: Posts the request to run after the current operation completes
navigator.Post(NavigationMode.GoTo, new LoginRoute());
// Or manually:
Dispatcher.UIThread.Post(() => navigator.GoToAsync(new LoginRoute()));
🛠️ Diagnostics
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 in [Route(ViewModel=...)]. |
| 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. Use Dispatcher.UIThread.Post instead. |
| 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 and has a parameterless constructor. |
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Avalonia (>= 11.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
-
net8.0
- Avalonia (>= 11.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
-
net9.0
- Avalonia (>= 11.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 2.0.7 | 120 | 12/31/2025 | |
| 2.0.6 | 117 | 12/31/2025 | |
| 2.0.5 | 121 | 12/30/2025 | |
| 2.0.4 | 115 | 12/29/2025 | |
| 2.0.3 | 117 | 12/28/2025 | |
| 2.0.2 | 120 | 12/28/2025 | |
| 2.0.0 | 131 | 12/27/2025 | |
| 1.9.9 | 206 | 12/24/2025 | |
| 1.9.8.1 | 191 | 12/22/2025 | |
| 1.9.8 | 271 | 12/22/2025 | |
| 1.9.6 | 151 | 12/20/2025 | |
| 1.9.0 | 247 | 12/14/2025 | |
| 1.7.0 | 230 | 12/12/2025 | |
| 1.6.0 | 534 | 12/10/2025 | |
| 1.5.1 | 450 | 12/8/2025 | |
| 1.5.0 | 441 | 12/8/2025 | |
| 1.4.0 | 566 | 12/7/2025 |