maui-shell-navigation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

.NET MAUI Shell Navigation

.NET MAUI Shell 导航

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.
使用Shell在.NET MAUI应用中实现页面导航。Shell提供基于URI的导航、Flyout菜单、标签栏以及四层视觉层级——所有功能都可以在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
  • 用标签页或Flyout菜单搭建应用顶层导航
  • 使用
    GoToAsync
    以编程方式在页面间跳转
  • 通过查询参数或对象参数在页面间传递数据
  • 为压栈导航注册详情页路由
  • 用确认对话框实现导航守卫(例如提示未保存的更改)
  • 按页面自定义返回按钮行为

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)
  • 从外部URL或应用链接进行深度链接——请查看.NET MAUI深度链接文档
  • 导航目标页面上的数据绑定——请使用
    maui-data-binding
  • 页面和视图模型的依赖注入——请使用
    maui-dependency-injection
  • 不使用Shell、仅使用
    NavigationPage
    的应用(使用不同的导航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
  • AppShell.xaml
    作为根Shell的.NET MAUI项目
  • 用于互相跳转的页面(
    ContentPage
  • 不在视觉层级中的详情页的路由名称

Shell Visual Hierarchy

Shell视觉层级

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
Shell使用四层层级结构,每一层都包裹其下的层级:
Shell
 ├── FlyoutItem / TabBar          (顶层分组)
 │    ├── Tab                     (底部标签分组)
 │    │    ├── ShellContent        (页面槽 → ContentPage)
 │    │    └── ShellContent        (多个则为顶部标签)
 │    └── Tab
 └── FlyoutItem / TabBar
  • FlyoutItem — 展示在Flyout菜单中,包含
    Tab
    子项
  • TabBar — 没有Flyout入口的底部标签栏
  • Tab — 对
    ShellContent
    进行分组,多个子项会生成顶部标签
  • ShellContent — 每个都指向一个
    ContentPage

Implicit Conversion

隐式转换

You can omit intermediate wrappers. Shell auto-wraps:
You writeShell creates
ShellContent
only
FlyoutItem > Tab > ShellContent
Tab
only
FlyoutItem > Tab
ShellContent
in
TabBar
TabBar > Tab > ShellContent
你可以省略中间的包裹层,Shell会自动封装:
编写的内容Shell自动生成的结构
ShellContent
FlyoutItem > Tab > ShellContent
Tab
FlyoutItem > Tab
TabBar
中的
ShellContent
TabBar > Tab > ShellContent

Workflow: Set Up AppShell

工作流程:搭建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. Register detail-page routes in the
    AppShell
    constructor
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"
                          ContentTemplate="{DataTemplate views:DomesticCatsPage}" />
            <ShellContent Title="Wild"
                          ContentTemplate="{DataTemplate views:WildCatsPage}" />
        </Tab>
        <Tab Title="Dogs" Icon="dogs.png">
            <ShellContent ContentTemplate="{DataTemplate views:DogsPage}" />
        </Tab>
    </FlyoutItem>

    <TabBar>
        <ShellContent Title="Home" Icon="home.png"
                      ContentTemplate="{DataTemplate views:HomePage}" />
        <ShellContent Title="Settings" Icon="settings.png"
                      ContentTemplate="{DataTemplate views:SettingsPage}" />
    </TabBar>
</Shell>
csharp
// AppShell.xaml.cs
public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();
        Routing.RegisterRoute("animaldetails", typeof(AnimalDetailsPage));
        Routing.RegisterRoute("editanimal", typeof(EditAnimalPage));
    }
}
  1. 定义继承自
    Shell
    AppShell.xaml
  2. 添加
    FlyoutItem
    TabBar
    元素用于顶层导航
  3. 添加
    Tab
    元素用于底部标签;嵌套多个
    ShellContent
    用于顶部标签
  4. 始终配合
    DataTemplate
    使用
    ContentTemplate
    ,这样页面会按需加载
  5. AppShell
    构造函数中注册详情页路由
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"
                          ContentTemplate="{DataTemplate views:DomesticCatsPage}" />
            <ShellContent Title="Wild"
                          ContentTemplate="{DataTemplate views:WildCatsPage}" />
        </Tab>
        <Tab Title="Dogs" Icon="dogs.png">
            <ShellContent ContentTemplate="{DataTemplate views:DogsPage}" />
        </Tab>
    </FlyoutItem>

    <TabBar>
        <ShellContent Title="Home" Icon="home.png"
                      ContentTemplate="{DataTemplate views:HomePage}" />
        <ShellContent Title="Settings" Icon="settings.png"
                      ContentTemplate="{DataTemplate views:SettingsPage}" />
    </TabBar>
</Shell>
csharp
// AppShell.xaml.cs
public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();
        Routing.RegisterRoute("animaldetails", typeof(AnimalDetailsPage));
        Routing.RegisterRoute("editanimal", typeof(EditAnimalPage));
    }
}

Workflow: Navigate with GoToAsync

工作流程:使用GoToAsync导航

All programmatic navigation uses
Shell.Current.GoToAsync
. Always
await
the call.
所有编程式导航都使用
Shell.Current.GoToAsync
,请始终
await
该调用。

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
前缀含义
//
从Shell根路径出发的绝对路由
(无)相对路径,将页面压入当前导航栈
..
返回上一级
../
返回上一级后再向前导航

Navigation Examples

导航示例

csharp
// 1. Absolute — switch to a specific hierarchy location
await Shell.Current.GoToAsync("//animals/cats/domestic");

// 2. Relative — push a registered detail page
await Shell.Current.GoToAsync("animaldetails");

// 3. With query string parameters
await Shell.Current.GoToAsync($"animaldetails?id={animal.Id}");

// 4. Go back one page
await Shell.Current.GoToAsync("..");

// 5. Go back two pages
await Shell.Current.GoToAsync("../..");

// 6. Go back one page, then push a different page
await Shell.Current.GoToAsync("../editanimal");
csharp
// 1. 绝对路径 — 切换到指定层级位置
await Shell.Current.GoToAsync("//animals/cats/domestic");

// 2. 相对路径 — 压入一个已注册的详情页
await Shell.Current.GoToAsync("animaldetails");

// 3. 携带查询字符串参数
await Shell.Current.GoToAsync($"animaldetails?id={animal.Id}");

// 4. 返回上一页
await Shell.Current.GoToAsync("..");

// 5. 返回两页
await Shell.Current.GoToAsync("../..");

// 6. 返回上一页,然后压入另一个页面
await Shell.Current.GoToAsync("../editanimal");

Workflow: Pass Data Between Pages

工作流程:在页面间传递数据

Option 1: IQueryAttributable (Preferred)

方案1:IQueryAttributable(推荐)

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();
    }
}
在视图模型上实现该接口,一次性接收所有参数:
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

方案2:QueryProperty特性

Apply directly on the page class:
csharp
[QueryProperty(nameof(AnimalId), "id")]
public partial class AnimalDetailsPage : ContentPage
{
    public string AnimalId { get; set; }
}
直接应用在页面类上:
csharp
[QueryProperty(nameof(AnimalId), "id")]
public partial class AnimalDetailsPage : ContentPage
{
    public string AnimalId { get; set; }
}

Option 3: Complex Objects via ShellNavigationQueryParameters

方案3:通过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;
}
无需序列化为字符串即可传递对象:
csharp
var parameters = new ShellNavigationQueryParameters
{
    { "animal", selectedAnimal }
};
await Shell.Current.GoToAsync("animaldetails", parameters);
通过
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.cs
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();
    }
}
OnNavigating
中使用
GetDeferral()
进行异步检查(例如“是否放弃未保存的更改?”):
csharp
// 写在AppShell.xaml.cs中
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();
    }
}

Tab Configuration

标签页配置

Bottom Tabs

底部标签页

Multiple
ShellContent
(or
Tab
) children inside a
TabBar
or
FlyoutItem
produce bottom tabs.
TabBar
FlyoutItem
中的多个
ShellContent
(或
Tab
)子项会生成底部标签页。

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
中的多个
ShellContent
子项会生成顶部标签页:
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.TabBarBackgroundColor
Color
Tab bar background
Shell.TabBarForegroundColor
Color
Selected icon color
Shell.TabBarTitleColor
Color
Selected tab title color
Shell.TabBarUnselectedColor
Color
Unselected tab icon/title
Shell.TabBarIsVisible
bool
Show/hide the tab bar
xml
<!-- Hide the tab bar on a specific page -->
<ContentPage Shell.TabBarIsVisible="False" ... />
附加属性类型用途
Shell.TabBarBackgroundColor
Color
标签栏背景色
Shell.TabBarForegroundColor
Color
选中的图标颜色
Shell.TabBarTitleColor
Color
选中的标签标题颜色
Shell.TabBarUnselectedColor
Color
未选中的标签图标/标题颜色
Shell.TabBarIsVisible
bool
显示/隐藏标签栏
xml
<!-- 在指定页面隐藏标签栏 -->
<ContentPage Shell.TabBarIsVisible="False" ... />

Flyout Configuration

Flyout配置

FlyoutBehavior

FlyoutBehavior

Set on
Shell
:
Disabled
,
Flyout
, or
Locked
.
xml
<Shell FlyoutBehavior="Flyout"> ... </Shell>
Shell
上设置:
Disabled
Flyout
Locked
xml
<Shell FlyoutBehavior="Flyout"> ... </Shell>

FlyoutDisplayOptions

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>
控制子项在Flyout中的展示方式:
  • AsSingleItem
    (默认)—— 整个分组只展示一个Flyout入口
  • AsMultipleItems
    —— 每个子
    Tab
    都有独立的入口
xml
<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">
    <Tab Title="Cats" ... />
    <Tab Title="Dogs" ... />
</FlyoutItem>

MenuItem (Non-Navigation Flyout Entries)

MenuItem(非导航类Flyout入口)

xml
<MenuItem Text="Log Out"
          Command="{Binding LogOutCommand}"
          IconImageSource="logout.png" />
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
.
按页面自定义返回按钮:
xml
<Shell.BackButtonBehavior>
    <BackButtonBehavior Command="{Binding BackCommand}"
                       IconOverride="back_arrow.png"
                       TextOverride="Cancel"
                       IsVisible="True" />
</Shell.BackButtonBehavior>
属性:
Command
CommandParameter
IconOverride
TextOverride
IsVisible
IsEnabled

Inspecting Navigation State

检查导航状态

csharp
// Current URI location
string location = Shell.Current.CurrentState.Location.ToString();

// Current page
Page page = Shell.Current.CurrentPage;

// Navigation stack of the current tab
IReadOnlyList<Page> stack = Shell.Current.Navigation.NavigationStack;
csharp
// 当前URI位置
string location = Shell.Current.CurrentState.Location.ToString();

// 当前页面
Page page = Shell.Current.CurrentPage;

// 当前标签的导航栈
IReadOnlyList<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
.
AppShell
中重写:
csharp
protected override void OnNavigated(ShellNavigatedEventArgs args)
{
    base.OnNavigated(args);
    // args.Current, args.Previous, args.Source
}
ShellNavigationSource
取值:
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.
  • 页面提前创建:直接使用
    Content
    而不是配合
    DataTemplate
    使用
    ContentTemplate
    会导致Shell初始化时创建所有页面,拖慢启动速度。请始终使用
    ContentTemplate
  • 路由名称重复:如果路由名称与现有路由或视觉层级路由重复,
    Routing.RegisterRoute
    会抛出
    ArgumentException
    。应用中所有路由必须唯一。
  • 未注册就使用相对路由:除非你已经用
    Routing.RegisterRoute
    注册了
    somepage
    ,否则不能调用
    GoToAsync("somepage")
    。视觉层级的页面使用
    //
    开头的绝对路由。
  • 不等待GoToAsync执行:不
    await
    GoToAsync
    会导致竞态条件和静默失败。请始终
    await
    该调用。
  • 绝对路由路径错误:绝对路由必须匹配视觉层级的完整路径(
    //FlyoutItem/Tab/ShellContent
    )。错误的路径会静默不执行,不会抛出异常。
  • 直接操作Tab.Stack:导航栈是只读的,所有导航变更都应该使用
    GoToAsync
  • 异步守卫忘记调用
    GetDeferral()
    OnNavigating
    中的同步取消可以正常工作,但异步检查需要调用
    GetDeferral()
    /
    deferral.Complete()
    来避免竞态条件。

References

参考资料