Skip to content

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