| name | maui-shell-navigation |
| description | .NET MAUI Shell navigation guidance — Shell visual hierarchy, AppShell setup, tab bars, flyout menus, URI-based navigation with GoToAsync, route registration, query parameters, back navigation, and navigation events. USE FOR: "Shell navigation", "GoToAsync", "AppShell", "tab bar", "flyout menu", "route registration", "query parameters navigation", "back navigation", "Shell tabs", "URI navigation", "navigation events". DO NOT USE FOR: deep linking from external URLs (use maui-deep-linking), data binding on pages (use maui-data-binding), or dependency injection setup (use maui-dependency-injection).
|
.NET MAUI Shell Navigation
Key decisions
ContentTemplate — always use it
Always use ContentTemplate with DataTemplate so pages are created on demand.
Using Content directly creates all pages during Shell init, hurting startup time.
<ShellContent ContentTemplate="{DataTemplate views:HomePage}" />
<ShellContent>
<views:HomePage />
</ShellContent>
Passing data — prefer IQueryAttributable over QueryProperty
IQueryAttributable gives you all parameters in one call and works on ViewModels:
public class AnimalDetailsViewModel : ObservableObject, IQueryAttributable
{
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("id", out var id))
AnimalId = id.ToString();
}
}
For complex objects, use ShellNavigationQueryParameters to avoid serializing:
var parameters = new ShellNavigationQueryParameters
{
{ "animal", selectedAnimal }
};
await Shell.Current.GoToAsync("animaldetails", parameters);
Guarding navigation — async deferral pattern
Use GetDeferral() for async checks (e.g., "save unsaved changes?"):
protected override async void OnNavigating(ShellNavigatingEventArgs args)
{
base.OnNavigating(args);
if (hasUnsavedChanges && args.Source == ShellNavigationSource.Pop)
{
var deferral = args.GetDeferral();
bool discard = await ShowConfirmationDialog();
if (!discard)
args.Cancel();
deferral.Complete();
}
}
Common gotchas
-
Duplicate route names — Routing.RegisterRoute throws ArgumentException
if a route name is already registered or matches a visual hierarchy route.
Every route must be unique across the entire app.
-
Relative routes require registration — you cannot GoToAsync("somepage")
unless somepage was registered with Routing.RegisterRoute. Visual hierarchy
pages use absolute // routes instead.
-
Pages are created on demand — when using ContentTemplate, the page
constructor runs only on first navigation. Don't assume pages exist at startup.
-
Tab.Stack is read-only — you cannot manipulate the navigation stack directly;
use GoToAsync for all navigation changes.
-
GoToAsync is async — always await it — fire-and-forget navigation causes
race conditions and can silently fail:
Shell.Current.GoToAsync("details");
await Shell.Current.GoToAsync("details");
-
Route hierarchy matters — absolute routes must match the full path through
the visual hierarchy (//FlyoutItem/Tab/ShellContent). Getting the path
wrong produces silent no-ops, not exceptions.