Use Shell URI routing to replace manual NavigationPage pushes in Xamarin.Forms
Centralize Xamarin.Forms navigation with Shell URI routing. Register routes in AppShell.xaml, bind query parameters with QueryProperty, and use GoToAsync for consistent deep linking and back stack control across Android and iOS.
16 Dec 2025, 04:03 UTC

Problem: fragmented stacks and brittle deep links
Manual NavigationPage.PushAsync creates separate stacks per tab and makes deep linking fragile. The useful takeaway is to centralize navigation in Xamarin.Forms Shell with a declarative route table and URI-based navigation that works the same on Android and iOS.
Central route table instead of imperative pushes
Shell replaces imperative page construction with a hierarchy of Flyout, TabBar and Navigation stacks declared in AppShell.xaml. Routes are registered once and referenced by name. Navigation is done with Shell.Current.GoToAsync, which parses the URI, resolves the route, pushes or resets the stack, and binds query parameters before OnAppearing.
Mechanism in one worked configuration
AppShell.xaml route registration
Register the shell hierarchy and a named route for a details page. The route name must be unique and registered before first navigation.
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:local="clr-namespace:YourApp"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="YourApp.AppShell">
<TabBar>
<ShellContent Title="Home" Route="home" ContentTemplate="{DataTemplate local:HomePage}" />
<ShellContent Title="Items" Route="items" ContentTemplate="{DataTemplate local:ItemsPage}" />
</TabBar>
<!-- Routes not in the visual hierarchy -->
<ShellContent Route="details" ContentTemplate="{DataTemplate local:DetailsPage}" />
</Shell>
Route names are case-sensitive. Duplicate names produce build warnings and runtime ambiguity.
Details page query binding
Shell sets query parameters on a public property marked with QueryProperty before OnAppearing. Complex objects cannot be passed via URI.
using Xamarin.Forms;
[QueryProperty(nameof(ItemId), "itemId")]
public partial class DetailsPage : ContentPage
{
public string ItemId
{
get => itemId;
set
{
itemId = value;
// Resolve the item from a service or cache using ItemId
}
}
private string itemId;
}
The property requires a public setter. Shell parses details?itemId=42 and assigns the value.
Programmatic navigation
Call GoToAsync from code-behind or a ViewModel where Shell is available. No page instance is constructed manually.
// From ItemsPage or a ViewModel
await Shell.Current.GoToAsync("details?itemId=42");
Relative routes push onto the current stack. Absolute routes starting with // reset to the root of the app.
// Reset to Home tab then open details
await Shell.Current.GoToAsync("//home/details?itemId=42");
Absolute routes are useful for deep links and for controlling back behavior. Mixing absolute and relative routes can cause inconsistent hardware back and iOS swipe gestures.
Limits
- Shell behavior is version-sensitive between Xamarin.Forms 4.x and 5.x. Xamarin is in maintenance mode; .NET MAUI is the successor.
- No native support for nested modal stacks inside tabs. Modals should be opened with Shell.Current.GoToAsync("//...", true) or ModalAsync with awareness of the current route.
- Route names must be unique across the app and registered before first use. Navigating to an unregistered route throws at runtime.
- QueryProperty only supports simple string values. Pass identifiers and resolve data via a service or cache.
- Custom renderers can interfere with Shell flyout and tab bar appearance on Android and iOS.
Common mistakes
- Navigating to an unregistered route. Verify registration in AppShell.xaml and that the route string matches exactly.
- Using a private or read-only property for QueryProperty. Shell silently fails to set the value.
- Passing complex objects in the URI. Use an id and resolve it in the page.
- Assuming back stack is identical on Android and iOS. Hardware back on Android and swipe back on iOS pop relative routes differently; use absolute routes to reset when needed.
- Registering routes after first navigation. Register all routes in AppShell at startup.
How to check the result
Inspect Shell.Current.CurrentState to confirm registered routes and current location after navigation. Test deep link URIs on both Android and iOS to verify query parameter binding and back stack behavior. Review build warnings for duplicate routes and missing QueryProperty setters during compilation and runtime.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.