mcpField
M
maui-shell-navigation

maui-shell-navigation

github.com/dotnet開発5.8k3.3k 回インストール1 日前に更新
リポジトリ

maui-shell-navigation について

maui-shell-navigationは、Shellを使ったナビゲーションを構築する.NET MAUI開発者向けのリファレンススキルです。宣言的なタブやフライアウト、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 ベースのナビゲーション、フライアウトメニュー、タブバー、そして 4 階層のビジュアル階層を提供します。これらはすべて XAML で宣言的に設定できます。

使用する場合

  • タブやフライアウトメニューを使った、アプリのトップレベルナビゲーションを設定する
  • GoToAsync を使って、プログラムからページ間を移動する
  • クエリパラメーターやオブジェクトパラメーターを使って、ページ間でデータを渡す
  • プッシュナビゲーション用に詳細ページのルートを登録する
  • 確認ダイアログ(未保存の変更など)でナビゲーションを制御する
  • ページごとに戻るボタンの動作をカスタマイズする

使用しない場合

  • 外部 URL やアプリリンクからのディープリンク — .NET MAUI deep linking docsを参照してください
  • ナビゲーション先のページでのデータバインディング — maui-data-bindingを使用してください
  • ページやビューモデルの依存関係の注入 — maui-dependency-injectionを使用してください
  • Shell を使わずに NavigationPage を使用するアプリ(ナビゲーション API が異なります)

入力

  • ルート Shell として AppShell.xaml を使用する .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") を呼び出すこと。実行時に例外がスローされます
ナビゲーションパラメーターを受け取るViewModel に IQueryAttributable を実装しますPage に実装すること。状態が BindingContext から分離されます
オブジェクト全体を渡すShellNavigationQueryParameters を使いますオブジェクトをクエリ文字列にシリアル化すること
GoToAsync を呼び出す場合await を付けますfire-and-forget にすること。例外が握りつぶされ、ナビゲーションの競合が発生します
戻るナビゲーションの前に確認するShellNavigatingEventArgs.GetDeferral() … deferral.Complete() を使いますダイアログのタスクを同期的にブロックすること
戻るナビゲーションを検出するe.Source == ShellNavigationSource.Pop を確認しますすべてのナビゲーションが戻る操作だと想定すること

Shell アプリに対して NavigationPage / PushAsync を使う解決策を提案しないでください。また、ユーザーから依頼されていない限り、正常に動作している AppShell の階層を再構成しないでください。

的を絞りつつ、必要な内容を漏れなく回答してください。 話題に沿うことは、回答を簡潔にしすぎることではありません。ナビゲーションの変更を示す場合は、実行に必要な要素を含めてください。たとえば、AppShell.xaml のマークアップと Routing.RegisterRoute の呼び出し、または GoToAsync の呼び出しと、受け取り側の IQueryAttributable / [QueryProperty] のコードを示します。クエリ文字列と ShellNavigationQueryParameters のように、どちらも有効な方法がある場合は両方を示し、それぞれの用途を説明してください。ユーザーが補完しなければならないコードスニペットを 1 つだけ示すよりも、両方を提示するほうが適切です。

Shell のビジュアル階層

Shell は 4 階層の階層構造を使用します。各レベルは、その下のレベルを包みます。

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 内の ShellContentTabBar > Tab > ShellContent

ワークフロー:AppShell のセットアップ

  1. Shell を継承する AppShell.xaml を定義します
  2. トップレベルのナビゲーションに FlyoutItem または TabBar 要素を追加します
  3. 下部タブには Tab 要素を追加し、上部タブには複数の ShellContent をネストします
  4. 必ず DataTemplate を使った ContentTemplate を使用します。ページをオンデマンドで読み込むためです
  5. すべての ShellContent に明示的な Route を指定します(以下を参照)
  6. 詳細ページのルートを AppShell コンストラクターで登録します

すべての ShellContent に Route= を設定してください。 省略すると、MAUI は共有カウンターから名前を自動生成し、Routing.cs は D_FAULT_{TypeName}{n} を出力します。名前のない ShellContent が 3 つある実際のシェルでは、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 つ前の階層に戻ります
../戻ってから前方へナビゲートします
ナビゲーションの例
// 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(推奨)

すべてのパラメーターを 1 回の呼び出しで受け取るには、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 に保持され、複数のパラメーターを 1 回の呼び出しで処理できます。

[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" ... />

フライアウトの構成

FlyoutBehavior

Shell に設定します:Disabled、Flyout、または Locked。

<Shell FlyoutBehavior="Flyout"> ... </Shell>
FlyoutDisplayOptions

子要素をフライアウトに表示する方法を制御します。

  • AsSingleItem(既定)— グループに対してフライアウト項目を 1 つ表示します
  • AsMultipleItems — 子要素の Tab ごとに項目を表示します
<FlyoutItem Title="Animals" FlyoutDisplayOptions="AsMultipleItems">
    <Tab Title="Cats" ... />
    <Tab Title="Dogs" ... />
</FlyoutItem>
MenuItem(ナビゲーションを行わないフライアウト項目)
<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。

よくある落とし穴

  • ページの先行作成:DataTemplate を使った ContentTemplate ではなく、直接 Content を使うと、Shell の初期化時にすべてのページが作成され、起動時間が長くなります。必ず ContentTemplate を使用してください。
  • ルート名の重複:ルート名が既存のルートまたはビジュアル階層のルートと一致する場合、Routing.RegisterRoute は ArgumentException をスローします。アプリ内のすべてのルート名は一意である必要があります。
  • 登録されていない相対ルート:somepage が Routing.RegisterRoute で登録されていない限り、GoToAsync("somepage") は呼び出せません。ビジュアル階層のページでは絶対 // ルートを使用します。
  • GoToAsync の非同期呼び出しを待機しない:GoToAsync を待機しないと、競合状態や気づかれない失敗が発生します。必ず呼び出しを await してください。
  • 絶対ルートパスの誤り:絶対ルートは、ビジュアル階層を通る完全なパス(//FlyoutItem/Tab/ShellContent)と一致する必要があります。パスが誤っている場合、例外ではなく、何も行われません。
  • Tab.Stack の直接操作:ナビゲーションスタックは読み取り専用です。ナビゲーションの変更にはすべて GoToAsync を使用してください。
  • 非同期ガードでの GetDeferral() の呼び出し忘れ:OnNavigating での同期的なキャンセルは機能しますが、非同期チェックでは競合状態を避けるために GetDeferral() と deferral.Complete() が必要です。

参考資料

コメント