maui-shell-navigation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

.NET MAUI Shell Navigation

.NET MAUI Shell导航

Shell Visual Hierarchy

Shell视觉层级结构

Shell uses a four-level hierarchy. Each level wraps the one below it:
Shell
 ├── FlyoutItem / TabBar          (top-level navigation grouping)
 │    ├── Tab                     (bottom-tab grouping)
 │    │    ├── ShellContent        (page slot; points to a ContentPage)
 │    │    └── ShellContent        (creates top tabs within a bottom tab)
 │    └── Tab
 └── FlyoutItem / TabBar
  • FlyoutItem – appears in the flyout menu. Contains one or more
    Tab
    children.
  • TabBar – bottom tab bar with no flyout entry. Use when the app has no flyout.
  • Tab – groups
    ShellContent
    objects. Multiple
    ShellContent
    in one
    Tab
    produces top tabs.
  • ShellContent – each represents a
    ContentPage
    .
Shell采用四级层级结构,每一层级包裹下一层级:
Shell
 ├── FlyoutItem / TabBar          (顶级导航分组)
 │    ├── Tab                     (底部标签分组)
 │    │    ├── ShellContent        (页面插槽;指向ContentPage)
 │    │    └── ShellContent        (在底部标签内创建顶部标签)
 │    └── Tab
 └── FlyoutItem / TabBar
  • FlyoutItem – 显示在弹出菜单中,包含一个或多个
    Tab
    子项。
  • TabBar – 底部标签栏,无弹出菜单入口。适用于应用不需要弹出菜单的场景。
  • Tab – 对
    ShellContent
    对象进行分组。单个
    Tab
    中的多个
    ShellContent
    会生成顶部标签。
  • 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
This keeps simple apps concise while allowing full control when needed.
你可以省略中间的包装元素,Shell会自动进行包装:
你编写的代码Shell自动创建的结构
ShellContent
only
FlyoutItem > Tab > ShellContent
Tab
only
FlyoutItem > Tab
ShellContent
in
TabBar
TabBar > Tab > ShellContent
这让简单应用的代码更简洁,同时在需要时仍能提供完整的控制能力。

AppShell.xaml Setup

AppShell.xaml配置

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

ContentTemplate and Lazy Loading

ContentTemplate与懒加载

Always use
ContentTemplate
with
DataTemplate
so pages are created on demand. Using
Content
directly creates all pages during Shell init, hurting startup time.
请始终将
ContentTemplate
DataTemplate
配合使用,这样页面会在需要时才创建。直接使用
Content
会在Shell初始化时创建所有页面,影响启动速度。

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 within that bottom tab:
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>

TabBar Appearance (Attached Properties)

TabBar外观(附加属性)

Set these on any page or Shell element:
Attached PropertyTypePurpose
Shell.TabBarBackgroundColor
Color
Tab bar background
Shell.TabBarForegroundColor
Color
Foreground / selected icon color
Shell.TabBarTitleColor
Color
Selected tab title color
Shell.TabBarUnselectedColor
Color
Unselected tab icon/title color
Shell.TabBarDisabledColor
Color
Disabled tab color
Shell.TabBarIsVisible
bool
Show/hide the tab bar
xml
<ContentPage Shell.TabBarIsVisible="False" ... />
可在任意页面或Shell元素上设置以下属性:
附加属性类型用途
Shell.TabBarBackgroundColor
Color
标签栏背景色
Shell.TabBarForegroundColor
Color
前景色 / 选中图标颜色
Shell.TabBarTitleColor
Color
选中标签标题颜色
Shell.TabBarUnselectedColor
Color
未选中标签图标/标题颜色
Shell.TabBarDisabledColor
Color
禁用标签颜色
Shell.TabBarIsVisible
bool
显示/隐藏标签栏
xml
<ContentPage Shell.TabBarIsVisible="False" ... />

Flyout Configuration

弹出菜单配置

FlyoutBehavior

FlyoutBehavior

Set on
Shell
:
xml
<Shell FlyoutBehavior="Flyout"> ... </Shell>
Values:
Disabled
,
Flyout
,
Locked
.
Shell
上设置:
xml
<Shell FlyoutBehavior="Flyout"> ... </Shell>
可选值:
Disabled
Flyout
Locked

FlyoutDisplayOptions

FlyoutDisplayOptions

Controls how a
FlyoutItem
's children appear in the flyout:
xml
<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">
    <Tab Title="Cats" ... />
    <Tab Title="Dogs" ... />
</FlyoutItem>
  • AsSingleItem
    (default) – one flyout entry for the group.
  • AsMultipleItems
    – each child
    Tab
    gets its own flyout entry.
控制
FlyoutItem
的子项在弹出菜单中的显示方式:
xml
<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">
    <Tab Title="Cats" ... />
    <Tab Title="Dogs" ... />
</FlyoutItem>
  • AsSingleItem
    (默认)– 整个分组显示为一个弹出菜单条目。
  • AsMultipleItems
    – 每个子
    Tab
    单独显示为一个弹出菜单条目。

Flyout Item Template

弹出菜单项模板

Customize appearance with
Shell.ItemTemplate
. BindingContext exposes
Title
and
FlyoutIcon
(FlyoutItem) or
Text
and
IconImageSource
(MenuItem):
xml
<Shell.ItemTemplate>
    <DataTemplate>
        <Grid ColumnDefinitions="Auto,*" Padding="10">
            <Image Source="{Binding FlyoutIcon}" HeightRequest="24" />
            <Label Grid.Column="1" Text="{Binding Title}" VerticalTextAlignment="Center" />
        </Grid>
    </DataTemplate>
</Shell.ItemTemplate>
使用
Shell.ItemTemplate
自定义外观,BindingContext会暴露
Title
FlyoutIcon
(针对FlyoutItem)或
Text
IconImageSource
(针对MenuItem):
xml
<Shell.ItemTemplate>
    <DataTemplate>
        <Grid ColumnDefinitions="Auto,*" Padding="10">
            <Image Source="{Binding FlyoutIcon}" HeightRequest="24" />
            <Label Grid.Column="1" Text="{Binding Title}" VerticalTextAlignment="Center" />
        </Grid>
    </DataTemplate>
</Shell.ItemTemplate>

Replacing Flyout Content

替换弹出菜单内容

xml
<Shell.FlyoutContent>
    <CollectionView BindingContext="{x:Reference shell}"
                    ItemsSource="{Binding FlyoutItems}" />
</Shell.FlyoutContent>
xml
<Shell.FlyoutContent>
    <CollectionView BindingContext="{x:Reference shell}"
                    ItemsSource="{Binding FlyoutItems}" />
</Shell.FlyoutContent>

MenuItem (non-navigation flyout entries)

MenuItem(非导航类弹出菜单条目)

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

Route Registration

路由注册

Shell visual hierarchy items have implicit routes derived from their
Route
property (or type name). Detail pages not in the hierarchy must be registered:
csharp
// In AppShell constructor or MauiProgram
Routing.RegisterRoute("animaldetails", typeof(AnimalDetailsPage));
Routing.RegisterRoute("editanimal", typeof(EditAnimalPage));
Gotcha: Duplicate route names throw
ArgumentException
at registration time. Every route must be unique across the entire app.
Shell视觉层级中的项会根据其
Route
属性(或类型名称)生成隐式路由。未包含在层级中的详情页必须手动注册:
csharp
// 在AppShell构造函数或MauiProgram中
Routing.RegisterRoute("animaldetails", typeof(AnimalDetailsPage));
Routing.RegisterRoute("editanimal", typeof(EditAnimalPage));
注意事项: 重复的路由名称会在注册时抛出
ArgumentException
。整个应用中的每个路由必须唯一。

Navigation with GoToAsync

使用GoToAsync进行导航

All programmatic navigation goes through
Shell.Current.GoToAsync
:
csharp
// Absolute – navigate to a specific place in the hierarchy
await Shell.Current.GoToAsync("//animals/cats/domestic");

// Relative – push a registered page onto the navigation stack
await Shell.Current.GoToAsync("animaldetails");

// With query string
await Shell.Current.GoToAsync($"animaldetails?id={animal.Id}");
所有程序化导航都需通过
Shell.Current.GoToAsync
实现:
csharp
// 绝对路由 —— 导航至层级结构中的指定位置
await Shell.Current.GoToAsync("//animals/cats/domestic");

// 相对路由 —— 将已注册的页面推入导航栈
await Shell.Current.GoToAsync("animaldetails");

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

Absolute vs Relative Routes

绝对路由与相对路由

PrefixMeaning
//
Absolute route from Shell root
(none)Relative; pushes onto the current nav stack
..
Go back one level in the navigation stack
../
Go back then navigate forward
csharp
// Go back one page
await Shell.Current.GoToAsync("..");

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

// Go back one page, then navigate to edit
await Shell.Current.GoToAsync("../editanimal");
Gotcha: Relative routes work only for pages registered with
Routing.RegisterRoute
. You cannot push visual-hierarchy pages as relative routes.
前缀含义
//
从Shell根节点开始的绝对路由
无前缀相对路由;推入当前导航栈
..
返回导航栈中的上一级
../
返回后再向前导航
csharp
// 返回上一页
await Shell.Current.GoToAsync("..");

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

// 返回上一页,然后导航至编辑页面
await Shell.Current.GoToAsync("../editanimal");
注意事项: 相对路由仅适用于通过
Routing.RegisterRoute
注册的页面。无法将视觉层级中的页面作为相对路由推入。

Query Parameters

查询参数

QueryProperty Attribute

QueryProperty特性

csharp
[QueryProperty(nameof(AnimalId), "id")]
public partial class AnimalDetailsPage : ContentPage
{
    public string AnimalId { get; set; }
}

// Navigate with query string:
await Shell.Current.GoToAsync($"animaldetails?id={animal.Id}");
csharp
[QueryProperty(nameof(AnimalId), "id")]
public partial class AnimalDetailsPage : ContentPage
{
    public string AnimalId { get; set; }
}

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

IQueryAttributable Interface

IQueryAttributable接口

Preferred for ViewModels — gives you 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();
    }
}
The interface works on the page itself or on any object set as the page's
BindingContext
.
推荐在ViewModel中使用该接口,可一次性获取所有参数:
csharp
public class AnimalDetailsViewModel : ObservableObject, IQueryAttributable
{
    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        if (query.TryGetValue("id", out var id))
            AnimalId = id.ToString();
    }
}
该接口可在页面本身或任何被设置为页面
BindingContext
的对象上使用。

Passing Complex Objects

传递复杂对象

Use
ShellNavigationQueryParameters
(dictionary of
string
object
) to pass objects without serializing to strings:
csharp
var parameters = new ShellNavigationQueryParameters
{
    { "animal", selectedAnimal }  // pass the object directly
};
await Shell.Current.GoToAsync("animaldetails", parameters);
Receive via
IQueryAttributable
:
csharp
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
    Animal = query["animal"] as Animal;
}
使用
ShellNavigationQueryParameters
string
object
的字典)传递对象,无需序列化为字符串:
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;
}

Navigation Events

导航事件

Override in your
AppShell
:
csharp
protected override void OnNavigating(ShellNavigatingEventArgs args)
{
    base.OnNavigating(args);
    if (hasUnsavedChanges && args.Source == ShellNavigationSource.Pop)
        args.Cancel();  // prevent leaving
}

protected override void OnNavigated(ShellNavigatedEventArgs args)
{
    base.OnNavigated(args);
    // args.Current, args.Previous, args.Source
}
For async checks, use
args.GetDeferral()
→ do work →
deferral.Complete()
.
ShellNavigationSource
values:
Push
,
Pop
,
PopToRoot
,
Insert
,
Remove
,
ShellItemChanged
,
ShellSectionChanged
,
ShellContentChanged
,
Unknown
.
AppShell
中重写以下方法:
csharp
protected override void OnNavigating(ShellNavigatingEventArgs args)
{
    base.OnNavigating(args);
    if (hasUnsavedChanges && args.Source == ShellNavigationSource.Pop)
        args.Cancel();  // 阻止离开
}

protected override void OnNavigated(ShellNavigatedEventArgs args)
{
    base.OnNavigated(args);
    // args.Current, args.Previous, args.Source
}
如需异步检查,可使用
args.GetDeferral()
→ 执行操作 →
deferral.Complete()
ShellNavigationSource
可选值:
Push
Pop
PopToRoot
Insert
Remove
ShellItemChanged
ShellSectionChanged
ShellContentChanged
Unknown

Inspecting Navigation State

检查导航状态

csharp
// Current URI location
ShellNavigationState state = Shell.Current.CurrentState;
string location = state.Location.ToString();  // e.g. "//animals/cats/domestic"

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

// Navigation stack of the current tab
IReadOnlyList<Page> stack = Shell.Current.Navigation.NavigationStack;
csharp
// 当前URI位置
ShellNavigationState state = Shell.Current.CurrentState;
string location = state.Location.ToString();  // 示例:"//animals/cats/domestic"

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

// 当前标签的导航栈
IReadOnlyList<Page> stack = Shell.Current.Navigation.NavigationStack;

Back Button Behavior

返回按钮行为

Customize the back button per page:
xml
<Shell.BackButtonBehavior>
    <BackButtonBehavior Command="{Binding BackCommand}"
                       IconOverride="back_arrow.png"
                       TextOverride="Cancel" />
</Shell.BackButtonBehavior>
Properties:
Command
,
CommandParameter
,
IconOverride
,
TextOverride
,
IsVisible
,
IsEnabled
.
可在每个页面上自定义返回按钮:
xml
<Shell.BackButtonBehavior>
    <BackButtonBehavior Command="{Binding BackCommand}"
                       IconOverride="back_arrow.png"
                       TextOverride="Cancel" />
</Shell.BackButtonBehavior>
可用属性:
Command
CommandParameter
IconOverride
TextOverride
IsVisible
IsEnabled

Common Gotchas

常见注意事项

  1. Duplicate route names
    Routing.RegisterRoute
    throws
    ArgumentException
    if a route name is already registered or matches a visual hierarchy route.
  2. Relative routes require registration – you cannot
    GoToAsync("somepage")
    unless
    somepage
    was registered with
    Routing.RegisterRoute
    . Visual hierarchy pages use absolute
    //
    routes.
  3. Pages are created on demand – when using
    ContentTemplate
    , the page constructor runs only on first navigation. Don't assume pages exist at startup.
  4. Tab.Stack is read-only – you cannot manipulate the navigation stack directly; use
    GoToAsync
    for all navigation changes.
  5. GoToAsync is async – always
    await
    it. Fire-and-forget navigation causes race conditions and can silently fail.
  6. Route hierarchy matters – absolute routes must match the full path through the visual hierarchy (
    //FlyoutItem/Tab/ShellContent
    ).
  1. 重复路由名称 – 如果路由名称已注册或与视觉层级中的路由重复,
    Routing.RegisterRoute
    会抛出
    ArgumentException
  2. 相对路由需要注册 – 除非
    somepage
    已通过
    Routing.RegisterRoute
    注册,否则无法使用
    GoToAsync("somepage")
    。视觉层级中的页面需使用绝对
    //
    路由。
  3. 页面按需创建 – 使用
    ContentTemplate
    时,页面构造函数仅在首次导航时执行。不要假设页面在启动时就已存在。
  4. Tab.Stack为只读 – 无法直接操作导航栈;请使用
    GoToAsync
    进行所有导航变更。
  5. GoToAsync为异步方法 – 请始终使用
    await
    调用。无等待的导航会导致竞争条件,且可能静默失败。
  6. 路由层级至关重要 – 绝对路由必须与视觉层级的完整路径匹配(
    //FlyoutItem/Tab/ShellContent
    )。