NavigationFrame.Avalonia 2.0.7

dotnet add package NavigationFrame.Avalonia --version 2.0.7
                    
NuGet\Install-Package NavigationFrame.Avalonia -Version 2.0.7
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NavigationFrame.Avalonia" Version="2.0.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NavigationFrame.Avalonia" Version="2.0.7" />
                    
Directory.Packages.props
<PackageReference Include="NavigationFrame.Avalonia" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NavigationFrame.Avalonia --version 2.0.7
                    
#r "nuget: NavigationFrame.Avalonia, 2.0.7"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NavigationFrame.Avalonia@2.0.7
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NavigationFrame.Avalonia&version=2.0.7
                    
Install as a Cake Addin
#tool nuget:?package=NavigationFrame.Avalonia&version=2.0.7
                    
Install as a Cake Tool

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., HomePageHomeViewModel). Supports 4 distinct lookup strategies.
  • Advanced Layout System: Support for nested layouts (Master → Settings → UISettingsPage) using the [Layout] attribute.
  • Rich Lifecycle Management: Granular interfaces (INavigatingFrom, IPreloadable, INavigatedTo, etc.) to handle data loading, navigation guards, and cleanup.
  • Smart UX Features:
    • Built-in Transitions: Easy-to-use animations (Slide, Fade, Zoom) configurable globally or per-route.
    • Intelligent Progress Indicator: IsNavigating property with configurable ShowDelay and HideDelay to prevent flickering for fast navigations.
  • Dependency Injection Ready: Seamless integration with Microsoft.Extensions.DependencyInjection, including support for Scoped Services per page.
  • Developer Experience: Includes compile-time diagnostics to catch errors early (e.g., missing ViewModels, redundant configurations).
  • Extensible Authorization: Built-in support for route-level and UI-Element-level authorization using policies and roles.

🚀 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>();
  // 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>();
}

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>
  <Panel>
    
    <ContentControl Content="{Binding NavigationService.Content}" />

    
    <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());
    }
  }
}

📖 Usage Guide

Defining Pages

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"));
Custom Base Route

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.
    • Matching: You can specify MatchStrategy in NavOptions (ByValue, ByType, or ByReference).
  • Post(mode, route, options): Safe way to trigger navigation from within a lifecycle callback (prevents deadlock).

Layouts

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;
Nested Layouts

Create a hierarchy by specifying a parent layout for your layout.

// AuthLayout is wrapped by MainLayout
[Layout<MainLayout>]
public partial class AuthLayout : UserControl, IMountableControl;

ViewModel Wiring

The framework infers the ViewModel type using 4 strategies in order:

  1. Namespace.HomePageNamespace.HomeViewModel
  2. Namespace.Views.HomePageNamespace.ViewModels.HomeViewModel
  3. Namespace.HomePageNamespace.ViewModels.HomeViewModel (Appended .ViewModels)
  4. Global Search: Any class named 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;

Authorization

The framework provides a flexible authorization system that works at both the route level and the UI level.

1. Configure Authorization Delegate

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();
}


2. Route-Level Authorization

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.

3. UI-Level Authorization (Attached Properties)

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>
  
  <TextBox nav:Authorize.Enable="" />
  
  <TextBox nav:Authorize.Enable="CanEditData" />
  
  <TextBox nav:Authorize.Enable="CanEditData; Admin,Manager" />
  
  <TextBox nav:Authorize.Enable="Policy=CanEditData" />
  <TextBox nav:Authorize.Enable="Roles=Admin,Manager" />
  
  <TextBox nav:Authorize.Enable="Policy=CanEditData; Roles=Admin" />
  
  <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 **.
  
  <ContentControl Content="{Binding NavigationService.Content}"
                  nav:Authorize.Authorizer="{Binding NavigationService.Authorizer}"/>
4. Manual Authorization

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;
    }
    // ...
  }
}

5. Authorization Tips

  • The navigation framework cannot detect whether the current user has switched accounts. If the user has switched accounts, it's best to clear the navigation stack and return to the home page.

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
  }
}

Resource Release

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.

Transitions and Stack Behavior

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);

💡 Best Practices

1. Avoiding Deadlocks (Re-entrancy)

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
}

2. Handling Data Loading

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);
  }
}

3. DI Configuration

  • Use ScopedNavigationService rather than NavigationService.
  • Register ScopedNavigationService as a singleton/transient service, depend on your MainViewModel.
  • Register IViewFactory/ PageViewModel (s) / LayoutViewModel (s) as scoped/transient services.

🛠️ 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.
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.
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.