关于 maui-shell-navigation
maui-shell-navigation 是面向 .NET MAUI 开发者的 Shell 导航参考技能。它涵盖声明式标签页和飞出菜单、基于 URI 的路由、页面间传递数据及导航拦截。
主要功能
- 01Shell 层级与菜单设置
- 02URI 路由和 GoToAsync 导航
- 03查询参数与对象传递
- 04异步导航拦截
- 05标签页与飞出菜单配置
适用场景
- 01设置顶层标签页或飞出菜单
- 02导航到已注册的详情页
- 03在页面之间传递数据
- 04离开含未保存更改的页面前确认
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill maui-shell-navigation安装后在 Claude Code 中输入/maui-shell-navigation确认已加载。
附带文件references/shell-navigation-api.md
详细介绍
.NET MAUI Shell 导航
使用 Shell 在 .NET MAUI 应用中实现页面导航。Shell 提供基于 URI 的导航、飞出菜单、标签栏和四级视觉层次结构,所有内容均通过 XAML 声明式配置。
适用场景
- 设置带标签或飞出菜单的应用顶层导航
- 使用
GoToAsync以编程方式在页面间导航 - 通过查询参数或对象参数在页面间传递数据
- 注册用于推送导航的详情页路由
- 使用确认对话框保护导航(例如,处理未保存的更改)
- 自定义各页面的返回按钮行为
不适用场景
- 从外部 URL 或应用链接进行深层链接——请参阅 .NET MAUI 深层链接文档
- 在导航目标页面上进行数据绑定——使用
maui-data-binding - 为页面和视图模型使用依赖注入——使用
maui-dependency-injection - 使用
NavigationPage而不使用 Shell 的应用(导航 API 不同)
输入
- 使用
AppShell.xaml作为根 Shell 的 .NET MAUI 项目 - 要在其间导航的页面(
ContentPage) - 不在视觉层次结构中的详情页路由名称
会改变答案的规则
以下是一些容易出错的 Shell 专属决策。在与用户的问题相关时,请务必遵循。
| 情况 | 应这样做 | 不应这样做 |
|---|---|---|
在 AppShell.xaml 中声明页面 | 声明 xmlns:views="clr-namespace:MyApp.Views" 后,使用 <ShellContent ContentTemplate="{DataTemplate views:MyPage}" />——页面会在首次导航时创建 | 使用 <ShellContent><views:MyPage /></ShellContent>,这会在启动时创建每个页面 |
| 导航到不在视觉层次结构中的页面 | 先调用 Routing.RegisterRoute("details", typeof(DetailsPage)) | 调用未注册的 GoToAsync("details")——运行时会抛出异常 |
| 接收导航参数 | 在视图模型上实现 IQueryAttributable | 在页面上实现,这会导致状态与 BindingContext 分离 |
| 传递完整对象 | 使用 ShellNavigationQueryParameters | 将对象序列化到查询字符串中 |
任何 GoToAsync 调用 | 使用 await 等待 | 即发即弃——异常会被吞掉,还会引发导航竞态 |
| 返回导航前确认 | 使用 ShellNavigatingEventArgs.GetDeferral() … deferral.Complete() | 同步阻塞等待对话框任务 |
| 检测返回导航 | 检查 e.Source == ShellNavigationSource.Pop | 假定每次导航都是返回操作 |
对于 Shell 应用,不要提出 NavigationPage / PushAsync 方案;除非用户提出要求,也不要重构正常工作的 AppShell 层次结构。
回答应紧扣问题,但内容要完整。 紧扣主题不意味着回答要简短。展示导航变更时,应包含运行所需的代码:AppShell.xaml 标记和 Routing.RegisterRoute 调用,或 GoToAsync 调用和接收参数的 IQueryAttributable / [QueryProperty] 代码。如果查询字符串和 ShellNavigationQueryParameters 两种方式都适用,请同时展示并说明各自的适用场景——让用户仍需补全的单个代码片段不是好的答案。
Shell 视觉层次结构
Shell 使用四级层次结构。每一级都包含其下一级:
Shell
├── FlyoutItem / TabBar (top-level grouping)
│ ├── Tab (bottom-tab grouping)
│ │ ├── ShellContent (page slot → ContentPage)
│ │ └── ShellContent (multiple = top tabs)
│ └── Tab
└── FlyoutItem / TabBar
- FlyoutItem — 显示在飞出菜单中;包含
Tab子项 - TabBar — 底部标签栏,不显示飞出菜单入口
- Tab — 对
ShellContent分组;包含多个子项时会显示顶部标签 - ShellContent — 每项都指向一个
ContentPage
隐式转换
可以省略中间包装项。Shell 会自动包装:
| 你编写的内容 | Shell 创建的内容 |
|---|---|
仅有 ShellContent | FlyoutItem > Tab > ShellContent |
仅有 Tab | FlyoutItem > Tab |
TabBar 中的 ShellContent | TabBar > Tab > ShellContent |
工作流:设置 AppShell
- 定义一个继承自
Shell的AppShell.xaml - 添加
FlyoutItem或TabBar元素以实现顶层导航 - 添加
Tab元素以实现底部标签页;嵌套多个ShellContent以实现顶部标签页 - 始终使用
ContentTemplate和DataTemplate,以便按需加载页面 - 为每个
ShellContent指定显式的Route(见下文) - 在
AppShell构造函数中注册详情页路由
为每个
ShellContent设置Route=。 如果省略,MAUI 会根据共享计数器自动生成名称——Routing.cs会生成D_FAULT_{TypeName}{n}。包含三个未命名ShellContent元素的实际 Shell 会生成类似D_FAULT_ShellContent2和D_FAULT_ShellContent5的路由:这些数字并非连续,取决于此前构造了多少个 Shell 元素,并且会在重新排序或添加页面时变化。你无法据此编写稳定的绝对路由(如//dashboard)或深层链接。显式的Route="dashboard"则会始终保持稳定。
<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>
// AppShell.xaml.cs
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute("animaldetails", typeof(AnimalDetailsPage));
Routing.RegisterRoute("editanimal", typeof(EditAnimalPage));
}
}
工作流:使用 GoToAsync 导航
所有程序化导航都使用 Shell.Current.GoToAsync。始终使用 await 等待调用完成。
路由前缀
| 前缀 | 含义 |
|---|---|
// | 从 Shell 根开始的绝对路由 |
| (无) | 相对路由;推入当前导航堆栈 |
.. | 返回上一级 |
../ | 返回后再向前导航 |
导航示例
// 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");
工作流:在页面之间传递数据
选项 1:IQueryAttributable(首选)
在 ViewModel 上实现,以便通过一次调用接收所有参数:
public class AnimalDetailsViewModel : ObservableObject, IQueryAttributable
{
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("id", out var id))
AnimalId = id.ToString();
}
}
选项 2:QueryProperty 特性
应用于 ViewModel 类(或确实由页面持有状态时应用于页面)。
优先在 ViewModel 上使用 IQueryAttributable——它会将导航状态保留在
BindingContext 中,并通过一次调用处理多个参数:
[QueryProperty(nameof(AnimalId), "id")]
public partial class AnimalDetailsViewModel : ObservableObject
{
[ObservableProperty]
private string _animalId = string.Empty;
}
Shell 会在页面构造函数设置 BindingContext 之后 应用查询特性,
因此该属性必须发出更改通知——普通的自动属性会使绑定停留在初始值。
选项 3:通过 ShellNavigationQueryParameters 传递复杂对象
无需将对象序列化为字符串即可传递:
var parameters = new ShellNavigationQueryParameters
{
{ "animal", selectedAnimal }
};
await Shell.Current.GoToAsync("animaldetails", parameters);
通过 IQueryAttributable 接收:
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
Animal = query["animal"] as Animal;
}
工作流:守护导航
在 OnNavigating 中使用 GetDeferral() 进行异步检查(例如“是否保存未保存的更改?”):
// 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();
}
}
标签页配置
底部标签页
在 TabBar 或 FlyoutItem 中包含多个 ShellContent(或 Tab)子元素会生成底部标签页。
顶部标签页
在单个 Tab 中包含多个 ShellContent 子元素会生成顶部标签页:
<Tab Title="Photos">
<ShellContent Title="Recent" ContentTemplate="{DataTemplate views:RecentPage}" />
<ShellContent Title="Favorites" ContentTemplate="{DataTemplate views:FavoritesPage}" />
</Tab>
标签栏外观
| 附加属性 | 类型 | 用途 |
|---|---|---|
Shell.TabBarBackgroundColor | Color | 标签栏背景 |
Shell.TabBarForegroundColor | Color | 选中图标的颜色 |
Shell.TabBarTitleColor | Color | 选中标签页标题的颜色 |
Shell.TabBarUnselectedColor | Color | 未选中标签页图标/标题的颜色 |
Shell.TabBarIsVisible | bool | 显示/隐藏标签栏 |
<ContentPage Shell.TabBarIsVisible="False" ... />
Flyout 配置
FlyoutBehavior
在 Shell 上设置:Disabled、Flyout 或 Locked。
<Shell FlyoutBehavior="Flyout"> ... </Shell>
FlyoutDisplayOptions
控制子项在 Flyout 中的显示方式:
AsSingleItem(默认)— 为该组显示一个 Flyout 条目AsMultipleItems— 每个子级Tab都有自己的条目
<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">
<Tab Title="Cats" ... />
<Tab Title="Dogs" ... />
</FlyoutItem>
MenuItem(非导航 Flyout 条目)
<MenuItem Text="Log Out"
Command="{Binding LogOutCommand}"
IconImageSource="logout.png" />
返回按钮行为
按页面自定义返回按钮:
<Shell.BackButtonBehavior>
<BackButtonBehavior Command="{Binding BackCommand}"
IconOverride="back_arrow.png"
TextOverride="Cancel"
IsVisible="True" />
</Shell.BackButtonBehavior>
属性:Command、CommandParameter、IconOverride、TextOverride、IsVisible、IsEnabled。
检查导航状态
// 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;
导航事件
在 AppShell 中重写:
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。
常见问题
- 页面创建过早:直接使用
Content而不是搭配DataTemplate使用ContentTemplate,会在 Shell 初始化时创建所有页面,拖慢启动速度。请始终使用ContentTemplate。 - 路由名称重复:如果路由名称与现有路由或可视层次结构中的路由相同,
Routing.RegisterRoute会引发ArgumentException。应用中的每个路由都必须唯一。 - 未注册的相对路由:除非已通过
Routing.RegisterRoute注册somepage,否则不能调用GoToAsync("somepage")。可视层次结构中的页面使用以//开头的绝对路由。 - 未等待
GoToAsync:不等待GoToAsync会导致竞争条件和静默失败。请始终对调用使用await。 - 绝对路由路径错误:绝对路由必须与可视层次结构中的完整路径相匹配(
//FlyoutItem/Tab/ShellContent)。路径错误会导致静默无操作,而不会引发异常。 - 直接操作
Tab.Stack:导航堆栈是只读的。所有导航更改都应使用GoToAsync。 - 异步守卫中忘记调用
GetDeferral():在OnNavigating中同步取消是可行的,但异步检查需要调用GetDeferral()/deferral.Complete(),以避免竞争条件。
参考资料
references/shell-navigation-api.md— Shell 层次结构、路由、标签页、飞出菜单和导航的完整 API 参考- .NET MAUI Shell 导航
- .NET MAUI Shell 标签页
- .NET MAUI Shell 飞出菜单
- .NET MAUI Shell 页面