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 without Shell (different navigation API)
NavigationPage
- 从外部URL或应用链接进行深度链接——请查看.NET MAUI深度链接文档
- 导航目标页面上的数据绑定——请使用
maui-data-binding - 页面和视图模型的依赖注入——请使用
maui-dependency-injection - 不使用Shell、仅使用的应用(使用不同的导航API)
NavigationPage
Inputs
前置要求
- A .NET MAUI project with as the root shell
AppShell.xaml - Pages () to navigate between
ContentPage - Route names for detail pages not in the visual hierarchy
- 以作为根Shell的.NET MAUI项目
AppShell.xaml - 用于互相跳转的页面()
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 children
Tab - TabBar — bottom tab bar with no flyout entry
- Tab — groups ; multiple children produce top tabs
ShellContent - 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 write | Shell creates |
|---|---|
| |
| |
| |
你可以省略中间的包裹层,Shell会自动封装:
| 编写的内容 | Shell自动生成的结构 |
|---|---|
仅 | |
仅 | |
| |
Workflow: Set Up AppShell
工作流程:搭建AppShell
- Define inheriting from
AppShell.xamlShell - Add or
FlyoutItemelements for top-level navigationTabBar - Add elements for bottom tabs; nest multiple
Tabfor top tabsShellContent - Always use with
ContentTemplateso pages load on demandDataTemplate - Register detail-page routes in the constructor
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));
}
}- 定义继承自的
ShellAppShell.xaml - 添加或
FlyoutItem元素用于顶层导航TabBar - 添加元素用于底部标签;嵌套多个
Tab用于顶部标签ShellContent - 始终配合使用
DataTemplate,这样页面会按需加载ContentTemplate - 在构造函数中注册详情页路由
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 . Always the call.
Shell.Current.GoToAsyncawait所有编程式导航都使用,请始终该调用。
Shell.Current.GoToAsyncawaitRoute Prefixes
路由前缀
| Prefix | Meaning |
|---|---|
| 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 :
IQueryAttributablecsharp
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);通过接收:
IQueryAttributablecsharp
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
Animal = query["animal"] as Animal;
}Workflow: Guard Navigation
工作流程:导航守卫
Use in for async checks (e.g., "save unsaved changes?"):
GetDeferral()OnNavigatingcsharp
// 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();
}
}在中使用进行异步检查(例如“是否放弃未保存的更改?”):
OnNavigatingGetDeferral()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 (or ) children inside a or produce bottom tabs.
ShellContentTabTabBarFlyoutItemTabBarFlyoutItemShellContentTabTop Tabs
顶部标签页
Multiple children inside a single produce top tabs:
ShellContentTabxml
<Tab Title="Photos">
<ShellContent Title="Recent" ContentTemplate="{DataTemplate views:RecentPage}" />
<ShellContent Title="Favorites" ContentTemplate="{DataTemplate views:FavoritesPage}" />
</Tab>单个中的多个子项会生成顶部标签页:
TabShellContentxml
<Tab Title="Photos">
<ShellContent Title="Recent" ContentTemplate="{DataTemplate views:RecentPage}" />
<ShellContent Title="Favorites" ContentTemplate="{DataTemplate views:FavoritesPage}" />
</Tab>Tab Bar Appearance
标签栏外观
| Attached Property | Type | Purpose |
|---|---|---|
| | Tab bar background |
| | Selected icon color |
| | Selected tab title color |
| | Unselected tab icon/title |
| | Show/hide the tab bar |
xml
<!-- Hide the tab bar on a specific page -->
<ContentPage Shell.TabBarIsVisible="False" ... />| 附加属性 | 类型 | 用途 |
|---|---|---|
| | 标签栏背景色 |
| | 选中的图标颜色 |
| | 选中的标签标题颜色 |
| | 未选中的标签图标/标题颜色 |
| | 显示/隐藏标签栏 |
xml
<!-- 在指定页面隐藏标签栏 -->
<ContentPage Shell.TabBarIsVisible="False" ... />Flyout Configuration
Flyout配置
FlyoutBehavior
FlyoutBehavior
Set on : , , or .
ShellDisabledFlyoutLockedxml
<Shell FlyoutBehavior="Flyout"> ... </Shell>在上设置:、或。
ShellDisabledFlyoutLockedxml
<Shell FlyoutBehavior="Flyout"> ... </Shell>FlyoutDisplayOptions
FlyoutDisplayOptions
Controls how children appear in the flyout:
- (default) — one flyout entry for the group
AsSingleItem - — each child
AsMultipleItemsgets its own entryTab
xml
<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">
<Tab Title="Cats" ... />
<Tab Title="Dogs" ... />
</FlyoutItem>控制子项在Flyout中的展示方式:
- (默认)—— 整个分组只展示一个Flyout入口
AsSingleItem - —— 每个子
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: , , , , , .
CommandCommandParameterIconOverrideTextOverrideIsVisibleIsEnabled按页面自定义返回按钮:
xml
<Shell.BackButtonBehavior>
<BackButtonBehavior Command="{Binding BackCommand}"
IconOverride="back_arrow.png"
TextOverride="Cancel"
IsVisible="True" />
</Shell.BackButtonBehavior>属性:、、、、、。
CommandCommandParameterIconOverrideTextOverrideIsVisibleIsEnabledInspecting 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 :
AppShellcsharp
protected override void OnNavigated(ShellNavigatedEventArgs args)
{
base.OnNavigated(args);
// args.Current, args.Previous, args.Source
}ShellNavigationSourcePushPopPopToRootInsertRemoveShellItemChangedShellSectionChangedShellContentChangedUnknown在中重写:
AppShellcsharp
protected override void OnNavigated(ShellNavigatedEventArgs args)
{
base.OnNavigated(args);
// args.Current, args.Previous, args.Source
}ShellNavigationSourcePushPopPopToRootInsertRemoveShellItemChangedShellSectionChangedShellContentChangedUnknownCommon Pitfalls
常见陷阱
- Eager page creation: Using directly instead of
ContentwithContentTemplatecreates all pages at Shell init, hurting startup time. Always useDataTemplate.ContentTemplate - Duplicate route names: throws
Routing.RegisterRouteif a route name matches an existing route or a visual hierarchy route. Every route must be unique across the app.ArgumentException - Relative routes without registration: You cannot unless
GoToAsync("somepage")was registered withsomepage. Visual hierarchy pages use absoluteRouting.RegisterRouteroutes.// - Fire-and-forget GoToAsync: Not awaiting causes race conditions and silent failures. Always
GoToAsyncthe call.await - Wrong absolute route path: Absolute routes must match the full path through the visual hierarchy (). Wrong paths produce silent no-ops, not exceptions.
//FlyoutItem/Tab/ShellContent - Manipulating Tab.Stack directly: The navigation stack is read-only. Use for all navigation changes.
GoToAsync - Forgetting for async guards: Synchronous cancellation in
GetDeferral()works, but async checks requireOnNavigating/GetDeferral()to avoid race conditions.deferral.Complete()
- 页面提前创建:直接使用而不是配合
Content使用DataTemplate会导致Shell初始化时创建所有页面,拖慢启动速度。请始终使用ContentTemplate。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
参考资料
- — Full API reference for Shell hierarchy, routes, tabs, flyout, and navigation
references/shell-navigation-api.md - .NET MAUI Shell Navigation
- .NET MAUI Shell Tabs
- .NET MAUI Shell Flyout
- .NET MAUI Shell Pages
- —— Shell层级、路由、标签、Flyout和导航的完整API参考
references/shell-navigation-api.md - .NET MAUI Shell Navigation
- .NET MAUI Shell Tabs
- .NET MAUI Shell Flyout
- .NET MAUI Shell Pages