mcpField
M
maui-shell-navigation

maui-shell-navigation

github.com/dotnet开发5.8k3.3k 次安装1 天前更新
代码仓库

关于 maui-shell-navigation

maui-shell-navigation 是面向 .NET MAUI 开发者的 Shell 导航参考技能。它涵盖声明式标签页和飞出菜单、基于 URI 的路由、页面间传递数据及导航拦截。

主要功能

  1. 01Shell 层级与菜单设置
  2. 02URI 路由和 GoToAsync 导航
  3. 03查询参数与对象传递
  4. 04异步导航拦截
  5. 05标签页与飞出菜单配置

适用场景

  1. 01设置顶层标签页或飞出菜单
  2. 02导航到已注册的详情页
  3. 03在页面之间传递数据
  4. 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 创建的内容
仅有 ShellContentFlyoutItem > Tab > ShellContent
仅有 TabFlyoutItem > Tab
TabBar 中的 ShellContentTabBar > Tab > ShellContent

工作流:设置 AppShell

  1. 定义一个继承自 Shell 的 AppShell.xaml
  2. 添加 FlyoutItem 或 TabBar 元素以实现顶层导航
  3. 添加 Tab 元素以实现底部标签页;嵌套多个 ShellContent 以实现顶部标签页
  4. 始终使用 ContentTemplate 和 DataTemplate,以便按需加载页面
  5. 为每个 ShellContent 指定显式的 Route(见下文)
  6. 在 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.TabBarBackgroundColorColor标签栏背景
Shell.TabBarForegroundColorColor选中图标的颜色
Shell.TabBarTitleColorColor选中标签页标题的颜色
Shell.TabBarUnselectedColorColor未选中标签页图标/标题的颜色
Shell.TabBarIsVisiblebool显示/隐藏标签栏

<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(),以避免竞争条件。

参考资料

评论

支持 Markdown