Skip to content
Shiny .NET v4 is here with BLE Windows Support, Improved GPS, & More! Check It Out

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

Arguments are received on the target ViewModel via IQueryAttributable.ApplyQueryAttributes.

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();
MethodDescription
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
Navigate()Builds the URI and executes the navigation

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

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
};
PropertyTypeDescription
FromUristring?The current Shell location URI
FromViewModelobject?The source page’s ViewModel instance
ToUristringThe destination route URI
NavigationTypeNavigationTypePush, SetRoot, GoBack, PopToRoot, or SwitchShell
ParametersIReadOnlyDictionary<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
};
PropertyTypeDescription
ToUristringThe destination route URI
ToViewModelobject?The destination page’s ViewModel instance
NavigationTypeNavigationTypePush, SetRoot, GoBack, PopToRoot, or SwitchShell
ParametersIReadOnlyDictionary<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>();

Arguments passed during navigation 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;
}
}

This works for arguments passed via NavigateTo, GoBack, and PopToRoot.