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 children.
Tab - TabBar – bottom tab bar with no flyout entry. Use when the app has no flyout.
- Tab – groups objects. Multiple
ShellContentin oneShellContentproduces top tabs.Tab - 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 write | Shell creates |
|---|---|
| |
| |
| |
This keeps simple apps concise while allowing full control when needed.
你可以省略中间的包装元素,Shell会自动进行包装:
| 你编写的代码 | Shell自动创建的结构 |
|---|---|
| |
| |
| |
这让简单应用的代码更简洁,同时在需要时仍能提供完整的控制能力。
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 with so pages are created on demand.
Using directly creates all pages during Shell init, hurting startup time.
ContentTemplateDataTemplateContent请始终将与配合使用,这样页面会在需要时才创建。直接使用会在Shell初始化时创建所有页面,影响启动速度。
ContentTemplateDataTemplateContentTab Configuration
标签配置
Bottom Tabs
底部标签
Multiple (or ) children inside a or
produce bottom tabs.
ShellContentTabTabBarFlyoutItem在或内添加多个(或)子项,会生成底部标签。
TabBarFlyoutItemShellContentTabTop Tabs
顶部标签
Multiple children inside a single produce top tabs within
that bottom tab:
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>TabBar Appearance (Attached Properties)
TabBar外观(附加属性)
Set these on any page or Shell element:
| Attached Property | Type | Purpose |
|---|---|---|
| | Tab bar background |
| | Foreground / selected icon color |
| | Selected tab title color |
| | Unselected tab icon/title color |
| | Disabled tab color |
| | Show/hide the tab bar |
xml
<ContentPage Shell.TabBarIsVisible="False" ... />可在任意页面或Shell元素上设置以下属性:
| 附加属性 | 类型 | 用途 |
|---|---|---|
| | 标签栏背景色 |
| | 前景色 / 选中图标颜色 |
| | 选中标签标题颜色 |
| | 未选中标签图标/标题颜色 |
| | 禁用标签颜色 |
| | 显示/隐藏标签栏 |
xml
<ContentPage Shell.TabBarIsVisible="False" ... />Flyout Configuration
弹出菜单配置
FlyoutBehavior
FlyoutBehavior
Set on :
Shellxml
<Shell FlyoutBehavior="Flyout"> ... </Shell>Values: , , .
DisabledFlyoutLocked在上设置:
Shellxml
<Shell FlyoutBehavior="Flyout"> ... </Shell>可选值:、、。
DisabledFlyoutLockedFlyoutDisplayOptions
FlyoutDisplayOptions
Controls how a 's children appear in the flyout:
FlyoutItemxml
<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">
<Tab Title="Cats" ... />
<Tab Title="Dogs" ... />
</FlyoutItem>- (default) – one flyout entry for the group.
AsSingleItem - – each child
AsMultipleItemsgets its own flyout entry.Tab
控制的子项在弹出菜单中的显示方式:
FlyoutItemxml
<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">
<Tab Title="Cats" ... />
<Tab Title="Dogs" ... />
</FlyoutItem>- (默认)– 整个分组显示为一个弹出菜单条目。
AsSingleItem - – 每个子
AsMultipleItems单独显示为一个弹出菜单条目。Tab
Flyout Item Template
弹出菜单项模板
Customize appearance with . BindingContext exposes
and (FlyoutItem) or and (MenuItem):
Shell.ItemTemplateTitleFlyoutIconTextIconImageSourcexml
<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>使用自定义外观,BindingContext会暴露和(针对FlyoutItem)或和(针对MenuItem):
Shell.ItemTemplateTitleFlyoutIconTextIconImageSourcexml
<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
property (or type name). Detail pages not in the hierarchy must be registered:
Routecsharp
// In AppShell constructor or MauiProgram
Routing.RegisterRoute("animaldetails", typeof(AnimalDetailsPage));
Routing.RegisterRoute("editanimal", typeof(EditAnimalPage));Gotcha: Duplicate route names throw at registration time.
Every route must be unique across the entire app.
ArgumentExceptionShell视觉层级中的项会根据其属性(或类型名称)生成隐式路由。未包含在层级中的详情页必须手动注册:
Routecsharp
// 在AppShell构造函数或MauiProgram中
Routing.RegisterRoute("animaldetails", typeof(AnimalDetailsPage));
Routing.RegisterRoute("editanimal", typeof(EditAnimalPage));注意事项: 重复的路由名称会在注册时抛出。整个应用中的每个路由必须唯一。
ArgumentExceptionNavigation with GoToAsync
使用GoToAsync进行导航
All programmatic navigation goes through :
Shell.Current.GoToAsynccsharp
// 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.GoToAsynccsharp
// 绝对路由 —— 导航至层级结构中的指定位置
await Shell.Current.GoToAsync("//animals/cats/domestic");
// 相对路由 —— 将已注册的页面推入导航栈
await Shell.Current.GoToAsync("animaldetails");
// 携带查询字符串
await Shell.Current.GoToAsync($"animaldetails?id={animal.Id}");Absolute vs Relative Routes
绝对路由与相对路由
| Prefix | Meaning |
|---|---|
| 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
. You cannot push visual-hierarchy pages as relative routes.
Routing.RegisterRoute| 前缀 | 含义 |
|---|---|
| 从Shell根节点开始的绝对路由 |
| 无前缀 | 相对路由;推入当前导航栈 |
| 返回导航栈中的上一级 |
| 返回后再向前导航 |
csharp
// 返回上一页
await Shell.Current.GoToAsync("..");
// 返回上两页
await Shell.Current.GoToAsync("../..");
// 返回上一页,然后导航至编辑页面
await Shell.Current.GoToAsync("../editanimal");注意事项: 相对路由仅适用于通过注册的页面。无法将视觉层级中的页面作为相对路由推入。
Routing.RegisterRouteQuery 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();
}
}该接口可在页面本身或任何被设置为页面的对象上使用。
BindingContextPassing Complex Objects
传递复杂对象
Use (dictionary of → ) to pass
objects without serializing to strings:
ShellNavigationQueryParametersstringobjectcsharp
var parameters = new ShellNavigationQueryParameters
{
{ "animal", selectedAnimal } // pass the object directly
};
await Shell.Current.GoToAsync("animaldetails", parameters);Receive via :
IQueryAttributablecsharp
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
Animal = query["animal"] as Animal;
}使用( → 的字典)传递对象,无需序列化为字符串:
ShellNavigationQueryParametersstringobjectcsharp
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;
}Navigation Events
导航事件
Override in your :
AppShellcsharp
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 → do work → .
args.GetDeferral()deferral.Complete()ShellNavigationSourcePushPopPopToRootInsertRemoveShellItemChangedShellSectionChangedShellContentChangedUnknown在中重写以下方法:
AppShellcsharp
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()ShellNavigationSourcePushPopPopToRootInsertRemoveShellItemChangedShellSectionChangedShellContentChangedUnknownInspecting 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: , , , ,
, .
CommandCommandParameterIconOverrideTextOverrideIsVisibleIsEnabled可在每个页面上自定义返回按钮:
xml
<Shell.BackButtonBehavior>
<BackButtonBehavior Command="{Binding BackCommand}"
IconOverride="back_arrow.png"
TextOverride="Cancel" />
</Shell.BackButtonBehavior>可用属性:、、、、、。
CommandCommandParameterIconOverrideTextOverrideIsVisibleIsEnabledCommon Gotchas
常见注意事项
- Duplicate route names – throws
Routing.RegisterRouteif a route name is already registered or matches a visual hierarchy route.ArgumentException - Relative routes require registration – you cannot unless
GoToAsync("somepage")was registered withsomepage. Visual hierarchy pages use absoluteRouting.RegisterRouteroutes.// - Pages are created on demand – when using , the page constructor runs only on first navigation. Don't assume pages exist at startup.
ContentTemplate - Tab.Stack is read-only – you cannot manipulate the navigation stack directly;
use for all navigation changes.
GoToAsync - GoToAsync is async – always it. Fire-and-forget navigation causes race conditions and can silently fail.
await - Route hierarchy matters – absolute routes must match the full path through
the visual hierarchy ().
//FlyoutItem/Tab/ShellContent
- 重复路由名称 – 如果路由名称已注册或与视觉层级中的路由重复,会抛出
Routing.RegisterRoute。ArgumentException - 相对路由需要注册 – 除非已通过
somepage注册,否则无法使用Routing.RegisterRoute。视觉层级中的页面需使用绝对GoToAsync("somepage")路由。// - 页面按需创建 – 使用时,页面构造函数仅在首次导航时执行。不要假设页面在启动时就已存在。
ContentTemplate - Tab.Stack为只读 – 无法直接操作导航栈;请使用进行所有导航变更。
GoToAsync - GoToAsync为异步方法 – 请始终使用调用。无等待的导航会导致竞争条件,且可能静默失败。
await - 路由层级至关重要 – 绝对路由必须与视觉层级的完整路径匹配()。
//FlyoutItem/Tab/ShellContent