Maui Shell Navigation

作者 dotnet0608d8924cd3MIT5.5K 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫今天更新

Guide for implementing Shell-based navigation in .NET MAUI apps. Covers AppShell setup, visual hierarchy (FlyoutItem, TabBar, Tab, ShellContent), URI-based navigation with GoToAsync, route registration, query parameters, back navigation, flyout and tab configuration, navigation events, and navigation guards. Use when: setting up Shell navigation, adding tabs or flyout menus, navigating between pages with GoToAsync, passing parameters between pages, registering routes, customizing back button behavior, or guarding navigation with confirmation dialogs. Do not use for: deep linking from external URLs (see .NET MAUI deep linking documentation), data binding on pages (use maui-data-binding), dependency injection setup (use maui-dependency-injection), or NavigationPage-only apps that don't use Shell.

AI 產生的概覽

指導在 .NET MAUI 應用程式中實作以 Shell 為基礎的頁面導覽,涵蓋 AppShell 設定、路由、索引標籤、飛出式功能表和 GoToAsync。

功能
此技能提供在 .NET MAUI 應用程式中使用 Shell 建置導覽的指示。內容涵蓋 AppShell 的 XAML 階層結構(FlyoutItem、TabBar、Tab、ShellContent)、路由註冊、使用 GoToAsync 的 URI 式導覽、查詢參數與物件參數、返回導覽、飛出式功能表與索引標籤設定、導覽事件,以及導覽防護。它也列出常見陷阱,並附上一份 Shell 導覽 API 參考文件。
適用情境
適用於在 .NET MAUI 應用程式中設定 Shell 導覽、新增索引標籤或飛出式功能表、使用 GoToAsync 在頁面之間導覽、在頁面之間傳遞參數、註冊路由、自訂返回按鈕行為,或透過確認對話方塊防護導覽。不適用於外部 URL 的深層連結、頁面資料繫結、相依性插入設定,或僅使用 NavigationPage 的應用程式。
執行需求
需要一個以 AppShell.xaml 作為根 Shell 的 .NET MAUI 專案,以及用於導覽的 ContentPage 頁面。僅為指示文件,不隨附指令碼。包含一份參考文件。

.NET MAUI Shell Navigation

Implement page navigation in .NET MAUI apps using Shell. Shell provides URI-based navigation, a flyout menu, tab bars, and a four-level visual hierarchy — all configured declaratively in XAML.

When to Use

  • Setting up top-level app navigation with tabs or a flyout menu
  • Navigating between pages programmatically with GoToAsync
  • Passing data between pages via query parameters or object parameters
  • Registering detail-page routes for push navigation
  • Guarding navigation with confirmation dialogs (e.g., unsaved changes)
  • Customizing back button behavior per page

When Not to Use

  • Deep linking from external URLs or app links — see .NET MAUI deep linking docs
  • Data binding on navigation target pages — use maui-data-binding
  • Dependency injection for pages and view models — use maui-dependency-injection
  • Apps using NavigationPage without Shell (different navigation API)

Inputs

  • A .NET MAUI project with AppShell.xaml as the root shell
  • Pages (ContentPage) to navigate between
  • Route names for detail pages not in the visual hierarchy

Rules That Change the Answer

These are the Shell-specific decisions that are easy to get wrong. Apply them whenever they are relevant to what the user asked.

SituationDo thisNot this
Declaring pages in AppShell.xamlWith xmlns:views="clr-namespace:MyApp.Views" declared: <ShellContent ContentTemplate="{DataTemplate views:MyPage}" /> — the page is created on first navigation<ShellContent><views:MyPage /></ShellContent>, which constructs every page at startup
Navigating to a page not in the visual hierarchyRouting.RegisterRoute("details", typeof(DetailsPage)) firstCalling GoToAsync("details") unregistered — it throws at runtime
Receiving navigation parametersImplement IQueryAttributable on the ViewModelImplementing it on the Page, which splits state from the BindingContext
Passing a whole objectShellNavigationQueryParametersSerialising the object into the query string
Any GoToAsync callawait itFire-and-forget — exceptions are swallowed and navigation races
Confirming before back navigationShellNavigatingEventArgs.GetDeferral() … deferral.Complete()Blocking synchronously on the dialog task
Detecting back navigationCheck e.Source == ShellNavigationSource.PopAssuming every navigation is a back action

Do not propose NavigationPage / PushAsync solutions for a Shell app, and do not restructure a working AppShell hierarchy unless the user asked.

Answer narrowly, but completely. Staying on topic does not mean being terse. When you show a navigation change, include the pieces needed to run it: the AppShell.xaml markup and the Routing.RegisterRoute call, or the GoToAsync call and the receiving IQueryAttributable / [QueryProperty] code. Where two approaches are both valid (query string vs ShellNavigationQueryParameters), show both and say when each fits — a single snippet the user still has to complete is a worse answer.

Shell Visual Hierarchy

Shell uses a four-level hierarchy. Each level wraps the one below it:

Shell ├── FlyoutItem / TabBar          (top-level grouping) │    ├── Tab                     (bottom-tab grouping) │    │    ├── ShellContent        (page slot → ContentPage) │    │    └── ShellContent        (multiple = top tabs) │    └── Tab └── FlyoutItem / TabBar
  • FlyoutItem — appears in the flyout menu; contains Tab children
  • TabBar — bottom tab bar with no flyout entry
  • Tab — groups ShellContent; multiple children produce top tabs
  • ShellContent — each points to a ContentPage

Implicit Conversion

You can omit intermediate wrappers. Shell auto-wraps:

You writeShell creates
ShellContent onlyFlyoutItem > Tab > ShellContent
Tab onlyFlyoutItem > Tab
ShellContent in TabBarTabBar > Tab > ShellContent

Workflow: Set Up AppShell

  1. Define AppShell.xaml inheriting from Shell
  2. Add FlyoutItem or TabBar elements for top-level navigation
  3. Add Tab elements for bottom tabs; nest multiple ShellContent for top tabs
  4. Always use ContentTemplate with DataTemplate so pages load on demand
  5. Give every ShellContent an explicit Route (see below)
  6. Register detail-page routes in the AppShell constructor

Set Route= on every ShellContent. If you omit it, MAUI auto-generates a name from a shared counter — Routing.cs produces D_FAULT_{TypeName}{n}. A real shell with three unnamed ShellContent elements yields routes like D_FAULT_ShellContent2 and D_FAULT_ShellContent5: the numbers are not sequential, they depend on how many Shell elements were constructed first, and they shift when you reorder or add pages. You cannot write a stable absolute route (//dashboard) or deep link against that. An explicit Route="dashboard" is stable forever.

xml
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"       xmlns:views="clr-namespace:MyApp.Views"       x:Class="MyApp.AppShell"       FlyoutBehavior="Flyout">
    <FlyoutItem Title="Animals" Icon="animals.png">        <Tab Title="Cats">            <ShellContent Title="Domestic" Route="domesticcats"                          ContentTemplate="{DataTemplate views:DomesticCatsPage}" />            <ShellContent Title="Wild" Route="wildcats"                          ContentTemplate="{DataTemplate views:WildCatsPage}" />        </Tab>        <Tab Title="Dogs" Icon="dogs.png">            <ShellContent Route="dogs" ContentTemplate="{DataTemplate views:DogsPage}" />        </Tab>    </FlyoutItem>
    <TabBar>        <ShellContent Title="Home" Icon="home.png" Route="home"                      ContentTemplate="{DataTemplate views:HomePage}" />        <ShellContent Title="Settings" Icon="settings.png" Route="settings"                      ContentTemplate="{DataTemplate views:SettingsPage}" />    </TabBar></Shell>
csharp
// AppShell.xaml.cspublic partial class AppShell : Shell{    public AppShell()    {        InitializeComponent();        Routing.RegisterRoute("animaldetails", typeof(AnimalDetailsPage));        Routing.RegisterRoute("editanimal", typeof(EditAnimalPage));    }}

Workflow: Navigate with GoToAsync

All programmatic navigation uses Shell.Current.GoToAsync. Always await the call.

Route Prefixes

PrefixMeaning
//Absolute route from Shell root
(none)Relative; pushes onto the current nav stack
..Go back one level
../Go back then navigate forward

Navigation Examples

csharp
// 1. Absolute — switch to a specific hierarchy locationawait Shell.Current.GoToAsync("//animals/cats/domestic");
// 2. Relative — push a registered detail pageawait Shell.Current.GoToAsync("animaldetails");
// 3. With query string parametersawait Shell.Current.GoToAsync($"animaldetails?id={animal.Id}");
// 4. Go back one pageawait Shell.Current.GoToAsync("..");
// 5. Go back two pagesawait Shell.Current.GoToAsync("../..");
// 6. Go back one page, then push a different pageawait Shell.Current.GoToAsync("../editanimal");

Workflow: Pass Data Between Pages

Option 1: IQueryAttributable (Preferred)

Implement on ViewModels to receive all parameters in one call:

csharp
public class AnimalDetailsViewModel : ObservableObject, IQueryAttributable{    public void ApplyQueryAttributes(IDictionary<string, object> query)    {        if (query.TryGetValue("id", out var id))            AnimalId = id.ToString();    }}

Option 2: QueryProperty Attribute

Apply on the ViewModel class (or the page, if it genuinely owns the state). Prefer IQueryAttributable on the ViewModel — it keeps navigation state with the BindingContext and handles multiple parameters in one call:

csharp
[QueryProperty(nameof(AnimalId), "id")]public partial class AnimalDetailsViewModel : ObservableObject{    [ObservableProperty]    private string _animalId = string.Empty;}

Shell applies query attributes after the page constructor sets BindingContext, so the property must raise change notification — a plain auto-property leaves the binding stuck on its initial value.

Option 3: Complex Objects via ShellNavigationQueryParameters

Pass objects without serializing to strings:

csharp
var parameters = new ShellNavigationQueryParameters{    { "animal", selectedAnimal }};await Shell.Current.GoToAsync("animaldetails", parameters);

Receive via IQueryAttributable:

csharp
public void ApplyQueryAttributes(IDictionary<string, object> query){    Animal = query["animal"] as Animal;}

Workflow: Guard Navigation

Use GetDeferral() in OnNavigating for async checks (e.g., "save unsaved changes?"):

csharp
// In AppShell.xaml.csprotected 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();    }}

Tab Configuration

Bottom Tabs

Multiple ShellContent (or Tab) children inside a TabBar or FlyoutItem produce bottom tabs.

Top Tabs

Multiple ShellContent children inside a single Tab produce top tabs:

xml
<Tab Title="Photos">    <ShellContent Title="Recent"    ContentTemplate="{DataTemplate views:RecentPage}" />    <ShellContent Title="Favorites" ContentTemplate="{DataTemplate views:FavoritesPage}" /></Tab>

Tab Bar Appearance

Attached PropertyTypePurpose
Shell.TabBarBackgroundColorColorTab bar background
Shell.TabBarForegroundColorColorSelected icon color
Shell.TabBarTitleColorColorSelected tab title color
Shell.TabBarUnselectedColorColorUnselected tab icon/title
Shell.TabBarIsVisibleboolShow/hide the tab bar
xml
<!-- Hide the tab bar on a specific page --><ContentPage Shell.TabBarIsVisible="False" ... />

Flyout Configuration

FlyoutBehavior

Set on Shell: Disabled, Flyout, or Locked.

xml
<Shell FlyoutBehavior="Flyout"> ... </Shell>

FlyoutDisplayOptions

Controls how children appear in the flyout:

  • AsSingleItem (default) — one flyout entry for the group
  • AsMultipleItems — each child Tab gets its own entry
xml
<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">    <Tab Title="Cats" ... />    <Tab Title="Dogs" ... /></FlyoutItem>

MenuItem (Non-Navigation Flyout Entries)

xml
<MenuItem Text="Log Out"          Command="{Binding LogOutCommand}"          IconImageSource="logout.png" />

Back Button Behavior

Customize the back button per page:

xml
<Shell.BackButtonBehavior>    <BackButtonBehavior Command="{Binding BackCommand}"                       IconOverride="back_arrow.png"                       TextOverride="Cancel"                       IsVisible="True" /></Shell.BackButtonBehavior>

Properties: Command, CommandParameter, IconOverride, TextOverride, IsVisible, IsEnabled.

Inspecting Navigation State

csharp
// Current URI locationstring location = Shell.Current.CurrentState.Location.ToString();
// Current pagePage page = Shell.Current.CurrentPage;
// Navigation stack of the current tabIReadOnlyList<Page> stack = Shell.Current.Navigation.NavigationStack;

Navigation Events

Override in AppShell:

csharp
protected override void OnNavigated(ShellNavigatedEventArgs args){    base.OnNavigated(args);    // args.Current, args.Previous, args.Source}

ShellNavigationSource values: Push, Pop, PopToRoot, Insert, Remove, ShellItemChanged, ShellSectionChanged, ShellContentChanged, Unknown.

Common Pitfalls

  • Eager page creation: Using Content directly instead of ContentTemplate with DataTemplate creates all pages at Shell init, hurting startup time. Always use ContentTemplate.
  • Duplicate route names: Routing.RegisterRoute throws ArgumentException if a route name matches an existing route or a visual hierarchy route. Every route must be unique across the app.
  • Relative routes without registration: You cannot GoToAsync("somepage") unless somepage was registered with Routing.RegisterRoute. Visual hierarchy pages use absolute // routes.
  • Fire-and-forget GoToAsync: Not awaiting GoToAsync causes race conditions and silent failures. Always await the call.
  • Wrong absolute route path: Absolute routes must match the full path through the visual hierarchy (//FlyoutItem/Tab/ShellContent). Wrong paths produce silent no-ops, not exceptions.
  • Manipulating Tab.Stack directly: The navigation stack is read-only. Use GoToAsync for all navigation changes.
  • Forgetting GetDeferral() for async guards: Synchronous cancellation in OnNavigating works, but async checks require GetDeferral() / deferral.Complete() to avoid race conditions.

References

來源與署名

來源:dotnet/skills位於plugins/dotnet-maui/skills/maui-shell-navigation提交0608d89

授權條款: MIT

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架