.NET MAUI の Shell Navigation で「一覧ページでは常にハンバーガーメニューを表示したいのに、詳細画面を経由すると戻る矢印になってしまう」問題は、ルート(Flyout のトップ)扱いが崩れているのが原因です。本記事では原因の見分け方と、MVVM でも安全に直せる実装手順をまとめます。
現象:.NET MAUI Shell Navigationで一覧ページなのにハンバーガーメニューが戻る矢印に変わる
たとえば建物管理アプリを .NET MAUI(MVVM)で作っていて、Shell の Flyout(ハンバーガーメニュー)を使っているとき、次のような挙動に悩むことがあります。
- Flyout から
BuildingOverviewPage(建物一覧)へ移動した直後は、左上がハンバーガーのまま - 一覧から
ViewEditBuildingPage(表示/追加/編集)へ遷移 - そこから「一覧へ戻る」を押すと、
BuildingOverviewPageが表示されるのに左上が戻る矢印になっている - 戻る矢印を押すと、直前の View/Edit/Add に戻ってしまい、ユーザー体験が不自然になる
一覧ページはアプリの“入口”として扱いたいことが多いので、ここで戻る矢印が出るのはかなりストレスです。特に「一覧=常に Flyout のトップ」という設計のアプリでは、ハンバーガーメニューが出続けること自体が要件になります。
結論:一覧ページをFlyoutのルートとして扱わせ、ハンバーガーメニューを常に表示する
この問題の本質は、Shell が BuildingOverviewPage を「Flyout のルート(トップ)」ではなく「ナビゲーションスタック上の1ページ」と認識してしまっていることです。解決の要点は次の2つに集約できます。
- 一覧ページのルート定義を一本化する(“二重登録”をやめる)
- 一覧へ戻すときは絶対ルート(
//)で開き直して、スタックをリセットする
この2点を押さえるだけで、一覧ページは常に Flyout のトップとして扱われ、左上はハンバーガー表示が維持されます。
なぜ戻る矢印になるのか:Shell Navigationの「スタック」と「ルート」
.NET MAUI の Shell Navigation は、ざっくり言うと「見えている画面の階層(FlyoutItem / Tab / Section など)」と「その中で積み上がるナビゲーションスタック」が合体した仕組みです。左上のアイコンは、Shell が現在地をどう解釈しているかで切り替わります。
| Shellの判断 | 左上に出るもの | よくある状況 |
|---|---|---|
| 現在ページがセクションのルート(トップ) | ハンバーガー(Flyout ボタン) | Flyout からトップページへ切り替えた直後 |
| 現在ページがスタック上(戻れるページがある) | 戻る矢印(Back ボタン) | 詳細・編集などへ遷移した後/スタックに“積んだ”戻り方をした後 |
つまり、一覧ページで戻る矢印が出るのは「一覧ページがルートではない」か「一覧ページがルートに見えても、実は“積まれた一覧”になっている」どちらかです。今回の症状(戻る矢印を押すと View/Edit/Add に戻ってしまう)は、ほぼ間違いなく後者、つまり一覧へ戻る操作が“戻る”ではなく“一覧をもう一枚積む”動きになっているパターンです。
ありがちな原因:一覧ページをルーティングで二重登録している
よくあるのが次の状態です。
- AppShell.xaml に
<ShellContent Route="BuildingOverviewPage" ... />を定義している - さらに AppShell.xaml.cs で
Routing.RegisterRoute("buildingoverview", typeof(BuildingOverviewPage));を書いている
これだと一覧ページへの入口が複数できてしまい、意図せず「Flyout の切り替え」ではなく「スタックにプッシュする遷移」で一覧へ戻ってしまうケースが増えます。結果として一覧ページの左上が戻る矢印になり、押すと直前ページへ戻ってしまいます。
まずはここから:戻る矢印問題の切り分けチェックリスト
修正に入る前に、現状のコードがどのパターンかを短時間で判断できるチェックリストを用意しました。
| チェック項目 | YESの場合に起こりやすいこと | 対処の方向性 |
|---|---|---|
一覧ページを ShellContent として定義している | 本来は Flyout のトップになれる | 戻り方(URL)を正すだけで直る可能性大 |
一覧ページを Routing.RegisterRoute でも登録している | 一覧へ戻るときに“積む”遷移をしがち | 一覧の RegisterRoute を削除して一本化 |
詳細→一覧の遷移が GoToAsync("BuildingOverviewPage") など相対/通常ルート | 一覧を新規にプッシュしてしまい戻る矢印が出る | GoToAsync("//BuildingOverviewPage") に置換 |
詳細→一覧が GoToAsync("..") なのに矢印が残る | そもそも一覧がルートになっていない可能性 | Shell の構造(FlyoutItem/Section)から見直す |
多くのケースは「一覧を RegisterRoute している」か「一覧に戻すときに // を使っていない」のどちらか、または両方です。
解決策:BuildingOverviewPageのRouteを一本化し、“二重登録”をやめる
一覧ページ(Flyout から到達する“トップ扱いの画面”)は、ShellContent の Route だけで管理するのが一番安全です。逆に、詳細・編集などの“スタックに積む画面”だけを Routing.RegisterRoute で登録する、という役割分担にすると迷いが減ります。
推奨:ShellContent に Route を付ける(一覧=トップ)
AppShell.xaml の例です。実際の構成に合わせて FlyoutItem や Title は調整してください。
<?xml version="1.0" encoding="utf-8" ?>
<Shell
x:Class="MyApp.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:MyApp.Views">
<FlyoutItem Title="建物" Route="buildings">
<ShellContent
Title="一覧"
Route="BuildingOverviewPage"
ContentTemplate="{DataTemplate views:BuildingOverviewPage}" />
</FlyoutItem>
</Shell>
ポイントは Route="BuildingOverviewPage" を明示して、一覧ページを Shell の階層に組み込むことです。これで Flyout から一覧へ移動したときは、一覧がトップとして扱われ、ハンバーガーメニューが出ます。
詳細ページは RegisterRoute(詳細=スタックに積む)
AppShell.xaml.cs では、詳細ページ(View/Edit/Add)だけを登録します。一覧ページを登録しないのがコツです。
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// 詳細・編集など「積む」画面だけを登録
Routing.RegisterRoute(nameof(ViewEditBuildingPage), typeof(ViewEditBuildingPage));
// 一覧(BuildingOverviewPage)は ShellContent の Route で定義済みなので RegisterRoute しない
// Routing.RegisterRoute("buildingoverview", typeof(BuildingOverviewPage)); // ← これは削除
}
}
この役割分担ができると、ナビゲーション設計がかなりシンプルになります。ルートページ(Flyout の入口)に戻すときは Shell の階層へ、詳細へ入るときは RegisterRoute へ、という考え方です。
解決策:一覧へ戻すときは // の絶対ルートで GoToAsync してスタックをリセットする
次に「戻り方」を直します。GoToAsync の書き方で、Shell が「戻る」なのか「積む」なのかを判断するためです。結論は、一覧へ戻したいときは絶対ルート(//)で開き直すのが最も確実です。
GoToAsync の書き方と挙動の違い
| 書き方 | 意味 | スタックへの影響 | 一覧での見た目 | おすすめ用途 |
|---|---|---|---|---|
await Shell.Current.GoToAsync(nameof(ViewEditBuildingPage)); | 登録ルートへ遷移(プッシュ) | スタックに積む | 戻る矢印になりやすい | 一覧→詳細、一覧→編集 |
await Shell.Current.GoToAsync(".."); | ひとつ戻る(ポップ) | スタックから外す | ルートに戻ればハンバーガー | 純粋に直前へ戻す |
await Shell.Current.GoToAsync("//BuildingOverviewPage"); | ルートから開き直す(絶対ルート) | スタックをリセット | ハンバーガーメニューが維持される | 保存/完了後に必ず一覧へ戻す |
今回の要件は「一覧ページでは常にハンバーガーメニューを表示したい」なので、詳細・編集がどういう遷移で来たかに関係なく、戻り先を一覧のトップに固定するのが正解です。そのために //BuildingOverviewPage を使います。
実装例:保存・キャンセル後は必ず一覧のトップへ
たとえば ViewEditBuildingPage 側で「保存」「キャンセル」「削除」などの完了後、一覧へ戻すときは次のようにします。
// 保存完了後など
await Shell.Current.GoToAsync("//BuildingOverviewPage");
これでナビゲーションスタックがリセットされ、BuildingOverviewPage は Flyout のトップとして扱われます。結果として、左上はハンバーガーメニューになり、押せば Flyout を開ける状態が維持されます。
ルートが見つからない場合は「完全なルート」を使う
アプリが複数の FlyoutItem / Tab を持つ場合、Route 名が重複したり、Shell の階層が深くなったりして、//BuildingOverviewPage だけだと意図した場所に行かないことがあります。その場合は、FlyoutItem の Route を含めた“完全なルート”に寄せると安定します。
// FlyoutItem に Route="buildings" を付けている場合の例
await Shell.Current.GoToAsync("//buildings/BuildingOverviewPage");
「どの Route をどう付けるべきか」で迷ったら、Route 名をユニークにする、そして戻すときは完全なルートで指定する、の2つが安全策です。
MVVM で迷子にならないための実装パターン
MVVM で Shell Navigation を扱うとき、ルート文字列が散らばるとメンテナンスが一気に難しくなります。ここでは、実務で事故が減りやすい構成を紹介します。
ルート文字列は定数に集約する
まずは「どこへ遷移するか」を 1 か所にまとめます。文字列の打ち間違いや、後から Route 名を変えたときの漏れを防げます。
public static class AppRoutes
{
// ルート(トップ)
public const string BuildingOverviewRoot = "//BuildingOverviewPage";
// 積む画面
public const string ViewEditBuilding = nameof(ViewEditBuildingPage);
}
ViewModel からは NavigationService を経由する
ViewModel が直接 Shell.Current に触れるかどうかはチーム方針によりますが、「テストしやすさ」「責務の分離」を考えると、薄い NavigationService を用意しておくと便利です。
public interface INavigationService
{
Task GoToAsync(string route);
Task GoToAsync(string route, IDictionary<string, object> parameters);
}
public sealed class ShellNavigationService : INavigationService
{
public Task GoToAsync(string route)
=> Shell.Current.GoToAsync(route);
public Task GoToAsync(string route, IDictionary<string, object> parameters)
=> Shell.Current.GoToAsync(route, parameters);
}
そして ViewModel では「保存後は必ず一覧トップへ戻す」という意図を、コードで明確に表現できます。
public sealed class ViewEditBuildingViewModel
{
private readonly INavigationService _nav;
public ViewEditBuildingViewModel(INavigationService nav)
{
_nav = nav;
}
public async Task SaveAsync()
{
// 保存処理(省略)
// 完了後は必ず一覧トップへ
await _nav.GoToAsync(AppRoutes.BuildingOverviewRoot);
}
public async Task CancelAsync()
{
// キャンセルも一覧トップへ固定するなら同じ
await _nav.GoToAsync(AppRoutes.BuildingOverviewRoot);
}
}
「戻る=スタックを戻る」ではなく、「完了=一覧へ着地する」という業務フローをそのままルーティングに落とし込めるのが、絶対ルート遷移の強みです。
詳細画面への遷移:パラメータの渡し方も整理しておく
一覧→詳細(表示/追加/編集)では、建物IDなどのパラメータを渡すことが多いです。Shell Navigation では、Dictionary で渡す方法が分かりやすく、型安全な実装にも寄せやすいです。
// 一覧から選択した建物の詳細へ
await Shell.Current.GoToAsync(
AppRoutes.ViewEditBuilding,
new Dictionary<string, object>
{
["BuildingId"] = selectedBuilding.Id,
["Mode"] = "Edit"
});
受け取り側は IQueryAttributable を使うか、[QueryProperty] で受け取ります。どちらでもよいですが、複数パラメータや変換が絡むなら IQueryAttributable の方が拡張しやすいです。
public partial class ViewEditBuildingPage : ContentPage, IQueryAttributable
{
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("BuildingId", out var idObj) && idObj is int id)
{
// ViewModel にセットするなど
}
}
}
ここを整理しておくと「保存後は必ず一覧へ」などの遷移ルールを統一しやすくなります。
よくある落とし穴:戻り先が“一覧に見えて一覧ではない”
戻る矢印問題が再発しやすいのは、次のような実装が混ざったときです。
落とし穴:詳細から一覧へ GoToAsync("BuildingOverviewPage") している
// が付いていない通常の遷移は、状況によって「一覧をスタックに積む」動きになり得ます。見た目は一覧へ戻ったようでも、内部的には「詳細の上に一覧が乗っている」状態なので、左上は戻る矢印になります。
落とし穴:一覧を RegisterRoute して、その Route を使って戻っている
Routing.RegisterRoute で登録したページは、基本的に“スタックに積むための入口”として使うのが前提です。ここで一覧を登録してしまうと、一覧がトップのはずなのに、戻る矢印の対象になってしまいます。
落とし穴:一覧ページが複数の場所に存在し、Route 名が衝突している
複数モジュールを持つアプリだと、同名の一覧ページ(たとえば「一覧」)が複数あって Route 名が重複しがちです。Shell は Route 名で解決するため、意図せず別セクションの一覧へ飛んだり、そもそも見つからなかったりします。Route 名はユニークにし、可能なら //flyoutRoute/shellContentRoute のように完全指定するのが安全です。
応急処置:アイコンだけハンバーガーに“見せる”方法(非推奨)
どうしてもナビゲーションスタックを残したまま、見た目だけハンバーガーにしたい場合、BackButtonBehavior のアイコンを上書きしてコマンドを差し替える方法があります。ただし、これは「戻れる状態なのに戻れないUI」になりやすく、UX的に危険なので、基本はおすすめしません。
// BuildingOverviewPage のコンストラクタなどで
Shell.SetBackButtonBehavior(this, new BackButtonBehavior
{
IconOverride = "hamburger.png",
Command = new Command(() => Shell.Current.FlyoutIsPresented = true)
});
この方法だと、内部スタックは残ったままです。ユーザーが「戻りたい」と思ったときに戻れない、あるいは端末の戻るジェスチャーだけ効いてしまうなど、挙動が分裂します。「一覧はトップ」なら、見た目ではなくスタックを正す方が長期的に安全です。
デバッグのコツ:現在地(Route)をログに出して原因を確定する
修正しても戻る矢印が消えない場合は、現在の Route をログに出すと原因特定が早くなります。Shell には現在地を表す情報があり、遷移後に確認できます。
var location = Shell.Current.CurrentState.Location.OriginalString;
System.Diagnostics.Debug.WriteLine($"Current Route: {location}");
一覧へ戻ったつもりなのに、ログ上は .../ViewEditBuildingPage/BuildingOverviewPage のような形になっているなら「一覧を積んでいる」確率が高いです。戻り処理を // に寄せる、一覧の RegisterRoute を消す、Route 名の衝突を解消する、の順で確認していきます。
補足:Add/Edit 後の戻り先を統一すると運用が楽になる
現場でよくあるのが「キャンセルは .. で戻すけど、保存後は一覧へ戻したい」「追加後は別のページへ遷移したい」など、戻り先がケースごとにバラけるパターンです。ここでルールが曖昧だと、いつの間にか戻る矢印問題が再発します。
要件が「一覧ページでは常にハンバーガーメニューを表示する」なら、Add/Edit の完了系アクションは基本的に //BuildingOverviewPage に揃えてしまうのが実装も運用もシンプルです。もし“直前に戻したい”が要件なら .. を使い、一覧がトップであることを保証する設計(一覧を積まない)を徹底します。
まとめ:一覧ページをトップ扱いにすると、ハンバーガーメニューとShell Navigationが安定する
.NET MAUI の Shell Navigation で、一覧ページのハンバーガーメニューが戻る矢印に変わってしまう問題は、Shell の仕様として「スタックに積まれたページには戻る矢印が出る」ことが原因です。解決するには、一覧ページを Flyout のルートとして扱わせる実装に揃えるのが最短です。
- 一覧ページは
ShellContentの Route だけで定義し、Routing.RegisterRouteでの二重登録を避ける - 詳細・編集から一覧へ戻すときは
await Shell.Current.GoToAsync("//BuildingOverviewPage");でルートから開き直す - MVVM ではルート定数と NavigationService で遷移ルールを一元管理すると再発しにくい
この形にしておくと、一覧ページの左上は常にハンバーガーメニューになり、ユーザーは「どこにいても一覧からメニューへ戻れる」安心感のあるUIを体験できます。

コメント