Skip to content
Shiny.NET
Shiny MAUI Shell v7 - App Links, App Shortcuts, & Navigation Interception!Shortcut me to it

Shell | Navigation

All navigation in Shiny Shell goes through the INavigator interface. Inject it into your ViewModels via constructor injection.

public class MyViewModel(INavigator navigator)
{
// navigator is ready to use
}

Navigate to a registered route by name, optionally passing arguments as key-value tuples.

// Simple route navigation
await navigator.NavigateTo("details");
// With arguments
await navigator.NavigateTo("details", ("Id", 42), ("Name", "Allan"));

When using string-based routes with tuple args, arguments are received on the target ViewModel via IQueryAttributable.ApplyQueryAttributes. When using [ShellProperty], properties are set directly — no IQueryAttributable needed.

Navigate by ViewModel type with an optional configuration action for strongly-typed argument passing.

// Navigate to a ViewModel
await navigator.NavigateTo<DetailViewModel>();
// With strongly-typed property setup
await navigator.NavigateTo<DetailViewModel>(vm => vm.Id = 42);
// With additional route arguments
await navigator.NavigateTo<DetailViewModel>(
vm => vm.Id = 42,
("ExtraArg", "value")
);

Both NavigateTo overloads accept a relativeNavigation parameter (default true). When set to false, the URI is prefixed with // to navigate from the root, replacing the navigation stack.

// Relative navigation (default) — pushes onto the stack
await navigator.NavigateTo("details");
await navigator.NavigateTo<DetailViewModel>();
// Root navigation — resets to root and navigates
await navigator.NavigateTo("dashboard", relativeNavigation: false);
await navigator.NavigateTo<DashboardViewModel>(relativeNavigation: false);
// With configuration
await navigator.NavigateTo<DashboardViewModel>(
vm => vm.WelcomeMessage = "Hello!",
relativeNavigation: false
);

The INavigationBuilder provides a fluent API for constructing multi-segment navigation URIs in a single call. Create one via INavigator.CreateBuilder().

// Push three pages in one navigation: ChainPage/DetailPage/ChainPage
await navigator
.CreateBuilder()
.Add<ChainViewModel>(x => x.Text = "Page1")
.Add<DetailViewModel>(x => x.Id = 42)
.Add<ChainViewModel>(x => x.Text = "Page3")
.Navigate();
// Pop back 2 pages, then push a new page: ../../ChainPage
await navigator
.CreateBuilder()
.PopBack(2)
.Add<ChainViewModel>(x => x.Text = "After Pop")
.Navigate();
// Navigate from root: //ChainPage/DetailPage
await navigator
.CreateBuilder(fromRoot: true)
.Add<ChainViewModel>(x => x.Text = "Root Start")
.Add<DetailViewModel>(x => x.Id = 1)
.Navigate();
// Mix string routes and ViewModel-based segments
await navigator
.CreateBuilder()
.Add("settings")
.Add<DetailViewModel>(x => x.Id = 99)
.Navigate();
// Skip the navigation interceptors - for the navigation a guard itself performs
await navigator
.CreateBuilder()
.BypassInterceptors()
.Add<DetailViewModel>(x => x.Id = 42)
.Navigate();
Method Description
CreateBuilder(bool fromRoot) Creates a builder. fromRoot: true prefixes the URI with //
PopBack(int count) Adds .. pop segments. Must be called before any Add calls. Not valid with fromRoot: true
Add<TViewModel>() Adds a route segment for the ViewModel type
Add<TViewModel>(configure) Adds a route segment with a configure callback invoked when the page is created
Add(string routeName) Adds a raw route string segment
BypassInterceptors() Skips the navigation interceptors for this navigation. Callable anywhere in the chain
Navigate() Builds the URI and executes the navigation. Returns false when an interceptor cancelled it. Takes bypassInterceptors and cancellationToken

Navigate back one or more pages, optionally passing arguments to the previous ViewModel.

// Simple go back
await navigator.GoBack();
// Go back with arguments
await navigator.GoBack(("Result", "saved"), ("Timestamp", DateTime.UtcNow));
// Go back multiple pages
await navigator.GoBack(2);
// Go back multiple pages with arguments
await navigator.GoBack(2, ("Result", "saved"));

Pop the entire navigation stack back to the root page.

await navigator.PopToRoot();
// With arguments
await navigator.PopToRoot(("Status", "complete"));

INavigator.ShowDialog<TViewModel, T> presents a mapped Page/ViewModel pair as a dialog and awaits a strongly typed result from it, instead of pushing it onto the navigation stack. See ViewModel Dialogs for the full API — including the source-generated Show{Route}Dialog wrappers that need no type arguments.

var result = await navigator.ShowPickColorDialog(preset: "Violet");
if (result.TryGetValue(out var colour))
this.Selected = colour;

Navigating / Navigated are deliberately not raised for dialogs, and INavigationAware / INavigationConfirmation are not consulted — showing a dialog is not a navigation stack mutation, and a “are you sure you want to leave” guard firing on a dialog would be wrong.

Replace the entire active Shell at runtime. Useful for switching between different app experiences (e.g. tabbed layout, flyout menu, onboarding flow).

// Switch to a new Shell instance
await navigator.SwitchShell(new AdminShell());
// Switch to a Shell resolved from DI
await navigator.SwitchShell<AdminShell>();

The current Shell’s ViewModel receives OnNavigatingFrom before the switch, and the Navigating event fires with NavigationType.SwitchShell.

INavigator can set and clear numeric badges on tabs that already exist in the active Shell.

// By route
await navigator.SetTabBadge("Inbox", 3);
await navigator.ClearTabBadge("Inbox");
// By ViewModel mapping
await navigator.SetTabBadge<InboxViewModel>(7);
await navigator.ClearTabBadge<InboxViewModel>();

For route-based navigation from XAML, use the Navigate attached properties instead of a ViewModel command.

<Button Text="Open Detail"
shiny:Navigate.Route="Detail"
shiny:Navigate.ParameterKey="ItemId"
shiny:Navigate.ParameterValue="{Binding SelectedId}" />

RelativeNavigation defaults to true. Set it to false for root navigation:

<ToolbarItem Text="Home"
shiny:Navigate.Route="MainPage"
shiny:Navigate.RelativeNavigation="False" />

For multiple parameters:

<Button Text="Open Modal"
shiny:Navigate.Route="modal">
<shiny:Navigate.Parameters>
<shiny:NavigationParameters>
<shiny:NavigationParameter Key="Arg1" Value="{Binding NavArg}" />
<shiny:NavigationParameter Key="Arg2" Value="5" />
</shiny:NavigationParameters>
</shiny:Navigate.Parameters>
</Button>

INavigator exposes two events for observing navigation lifecycle:

Fires before navigation occurs. Provides the source ViewModel instance and destination route.

navigator.Navigating += (sender, args) =>
{
// args.FromUri — current location URI
// args.FromViewModel — source ViewModel instance (object?)
// args.ToUri — destination route URI
// args.NavigationType — Push, SetRoot, GoBack, PopToRoot, or SwitchShell
// args.Parameters — navigation parameters
};
Property Type Description
FromUri string? The current Shell location URI
FromViewModel object? The source page’s ViewModel instance
ToUri string The destination route URI
NavigationType NavigationType Push, SetRoot, GoBack, PopToRoot, or SwitchShell
Parameters IReadOnlyDictionary<string, object> Navigation parameters

Fires after navigation completes and the destination page’s ViewModel is resolved.

navigator.Navigated += (sender, args) =>
{
// args.ToUri — destination route URI
// args.ToViewModel — destination ViewModel instance (object?)
// args.NavigationType — Push, SetRoot, GoBack, PopToRoot, or SwitchShell
// args.Parameters — navigation parameters
};
Property Type Description
ToUri string The destination route URI
ToViewModel object? The destination page’s ViewModel instance
NavigationType NavigationType Push, SetRoot, GoBack, PopToRoot, or SwitchShell
Parameters IReadOnlyDictionary<string, object> Navigation parameters

Use IMauiInitializeService to hook events for cross-cutting concerns like logging or analytics:

public class NavigationLogger(
ILogger<NavigationLogger> logger,
INavigator navigator
) : IMauiInitializeService
{
public void Initialize(IServiceProvider services)
{
navigator.Navigating += (_, args) =>
{
logger.LogInformation(
"Navigating from '{FromUri}' to '{ToUri}' | Type: {NavigationType} | FromViewModel: {FromViewModel}",
args.FromUri,
args.ToUri,
args.NavigationType,
args.FromViewModel?.GetType().Name
);
};
navigator.Navigated += (_, args) =>
{
logger.LogInformation(
"Navigated to '{ToUri}' | Type: {NavigationType} | ToViewModel: {ToViewModel}",
args.ToUri,
args.NavigationType,
args.ToViewModel?.GetType().Name
);
};
}
}
// Register in MauiProgram.cs
builder.Services.AddSingleton<IMauiInitializeService, NavigationLogger>();

When using [ShellProperty], the source-generated navigation methods set properties directly on the ViewModel — no IQueryAttributable needed:

[ShellMap<DetailPage>("Detail")]
public partial class DetailViewModel : ObservableObject
{
[ShellProperty]
public int Id { get; set; }
}
// Generated method sets Id directly
await navigator.NavigateToDetail(id: 42);

For string-based NavigateTo(route, args) with tuple parameters, arguments are delivered via the MAUI built-in IQueryAttributable interface:

public class DetailViewModel : IQueryAttributable
{
public int Id { get; set; }
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("Id", out var id))
Id = (int)id;
}
}