自动化与脚本
需要实现 .NET MAUI Shell 导航时:配置…
在 .NET MAUI 应用中使用 Shell 实现选项卡/浮出菜单、URI 导航、页面间传参和导航保护。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
需要实现 .NET MAUI Shell特定
解决思路
- 按四级视觉层次组织 Shell:Shell > FlyoutItem/TabBar > Tab > ShellContent;FlyoutItem 出现在浮出菜单并包含 Tab,TabBar 是底部选项卡且不出现在浮出菜单,Tab 分组 ShellContent,ShellContent 指向 ContentPage。
- 利用 Shell 隐式包装:只写 ShellContent 时自动生成 FlyoutItem > Tab > ShellContent;只写 Tab 时生成 FlyoutItem > Tab;在 TabBar 中写 ShellContent 时生成 TabBar > Tab > ShellContent。
- 在 AppShell.xaml 中定义继承自 Shell 的根 Shell,并设置 x:Class 为 MyApp.AppShell。
- 用 FlyoutItem 或 TabBar 元素添加顶级导航分组;用 Tab 添加底部选项卡;在单个 Tab 内嵌套多个 ShellContent 形成顶部选项卡。
- 始终在 ShellContent 上使用 ContentTemplate="{DataTemplate views:MyPage}" 来声明页面;不要用 <ShellContent><views:MyPage /></ShellContent>,否则启动时会构造每个页面。
- 为每个 ShellContent 显式设置 Route="...",例如 Route="dashboard";省略 Route 会由 Routing.cs 生成不稳定的 D_FAULT_{TypeName}{n},无法写出稳定绝对路由或深链。
- 在 AppShell 构造函数中调用 InitializeComponent(),再对不在视觉层次中的详情页调用 Routing.RegisterRoute("animaldetails", typeof(AnimalDetailsPage)) 等注册路由。
- 声明页面命名空间时使用 xmlns:views="clr-namespace:MyApp.Views",并在 ShellContent 的 ContentTemplate 中引用 views:MyPage。
- 要导航到不在视觉层次中的页面,先用 Routing.RegisterRoute 注册;未注册就 GoToAsync("somepage") 会在运行时抛异常。
- 接收导航参数时在 ViewModel 上实现 IQueryAttributable;不要在 Page 上实现,以保持导航状态与 BindingContext 一致。
- 在 IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) 中用 query.TryGetValue("id", out var id) 读取参数,并赋值给 ViewModel 属性。
- 如果偏好特性方式,在 ViewModel 类上使用 [QueryProperty(nameof(AnimalId), "id")];页面真正拥有状态时才放在 Page 上。优先 IQueryAttributable,因为它可一次处理多个参数并保持状态在 BindingContext。
- Shell 会在页面构造函数设置 BindingContext 之后应用查询属性,因此被绑定的属性必须能引发变更通知;普通自动属性会让绑定停留在初始值。
- 传递整个对象时用 ShellNavigationQueryParameters,例如 new ShellNavigationQueryParameters { { "animal", selectedAnimal } },然后 await Shell.Current.GoToAsync("animaldetails", parameters);不要把对象序列化进查询字符串。
- 接收复杂对象时在 ApplyQueryAttributes 中执行 query["animal"] as Animal。
- 所有程序化导航都使用 await Shell.Current.GoToAsync(...);不要 fire-and-forget,否则异常被吞掉且导航发生竞态。
- 理解路由前缀:// 表示从 Shell 根开始的绝对路由;无前缀表示相对路由并压入当前导航栈;.. 表示返回一级;../ 表示返回后再向前导航。
- 绝对导航示例:await Shell.Current.GoToAsync("//animals/cats/domestic")。
- 相对导航示例:await Shell.Current.GoToAsync("animaldetails"),目标必须已用 Routing.RegisterRoute 注册。
- 带查询字符串参数示例:await Shell.Current.GoToAsync($"animaldetails?id={animal.Id}")。
- 返回一级示例:await Shell.Current.GoToAsync("..");返回两级示例:await Shell.Current.GoToAsync("../..");返回一级再推入其他页面示例:await Shell.Current.GoToAsync("../editanimal")。
- 需要异步确认后再返回时,在 AppShell.xaml.cs 重写 protected override async void OnNavigating(ShellNavigatingEventArgs args),先调用 base.OnNavigating(args)。
- 在 OnNavigating 中检查 hasUnsavedChanges 且 args.Source == ShellNavigationSource.Pop;满足时用 args.GetDeferral() 获取延迟,await 确认对话框,若用户不丢弃则 args.Cancel(),最后调用 deferral.Complete()。
- 检测返回导航时检查 e.Source == ShellNavigationSource.Pop;不要假设每次导航都是返回操作。
- 配置底部选项卡:在 TabBar 或 FlyoutItem 内放置多个 ShellContent 或 Tab 子项。
- 配置顶部选项卡:在单个 Tab 内放置多个 ShellContent 子项。
- 设置选项卡外观:使用附加属性 Shell.TabBarBackgroundColor、Shell.TabBarForegroundColor、Shell.TabBarTitleColor、Shell.TabBarUnselectedColor、Shell.TabBarIsVisible。
- 在特定页面隐藏选项卡栏:在 ContentPage 上设置 Shell.TabBarIsVisible="False"。
- 设置 Shell 的 FlyoutBehavior 为 Disabled、Flyout 或 Locked。
- 用 FlyoutDisplayOptions 控制浮出菜单子项:AsSingleItem(默认,一个组条目)或 AsMultipleItems(每个子 Tab 各自条目)。
- 在浮出菜单中添加非导航条目:使用 <MenuItem Text="Log Out" Command="{Binding LogOutCommand}" IconImageSource="logout.png" />。
- 自定义返回按钮:在页面中设置 Shell.BackButtonBehavior,内部使用 BackButtonBehavior 的 Command、CommandParameter、IconOverride、TextOverride、IsVisible、IsEnabled。
- 检查当前导航状态:用 Shell.Current.CurrentState.Location.ToString() 获取当前 URI;用 Shell.Current.CurrentPage 获取当前页面;用 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。
- 避免 eager page creation:不要用 Content 直接放页面,始终用 ContentTemplate 加 DataTemplate。
- 避免重复路由名:Routing.RegisterRoute 在路由名与已有路由或视觉层次路由冲突时抛 ArgumentException;每个路由必须在整个应用中唯一。
- 不要对未注册的页面使用相对 GoToAsync;视觉层次中的页面使用 // 绝对路由。
- 不要 fire-and-forget GoToAsync;始终 await。
- 绝对路由必须匹配完整视觉层次路径(//FlyoutItem/Tab/ShellContent);错误路径会静默 no-op 而不是抛异常。
- 不要直接操作 Tab.Stack;导航栈只读,所有导航变化都用 GoToAsync。
- 异步导航守卫不要忘记 GetDeferral()/deferral.Complete();同步取消可直接在 OnNavigating 中取消,但异步检查需要延迟完成以避免竞态。
- 不要对 Shell 应用提出 NavigationPage / PushAsync 方案;除非用户要求,不要重构可工作的 AppShell 层级。
- 回答导航变更时要窄而完整:展示 AppShell.xaml 标记和对应的 Routing.RegisterRoute 调用,或展示 GoToAsync 调用和接收端的 IQueryAttributable/[QueryProperty] 代码。
- 当查询字符串和 ShellNavigationQueryParameters 都可行时,两种都展示并说明各自适用场景。
- 需要完整 API 时查看 references/shell-navigation-api.md 以及官方 Shell Navigation、Tabs、Flyout、Pages 文档。
适用边界 · 注意事项
- 不要用于从外部 URL 或应用链接进行 deep linking;参见 .NET MAUI deep linking 文档。
- 不要用于导航目标页的数据绑定或页面/ViewModel 依赖注入设置;分别使用 maui-data-binding 和 maui-dependency-injection。
- 不要用于不使用 Shell、仅使用 NavigationPage 的应用(导航 API 不同)。