.NET MAUI の Shell でフライアウト(ハンバーガーメニュー)を組むと、FlyoutItem と ShellContent の境目に灰色の区切り線が突然入り、意図しない「別グループ」表示になることがあります。見た目の違和感だけでなく、ShellContent にページが無いと例外の原因にもなるため、仕組みを整理しつつ、要件を崩さずに解消する実装パターンをまとめます。
.NET MAUI Shell のフライアウトで起きる「灰色の区切り線」問題とは
Shell を採用すると、フライアウトにページ(ナビゲーション先)を並べるだけでなく、タブ構成や階層をまとめて宣言できます。一方で、構成の組み合わせによっては、フライアウト内でメニューが自動的に「グルーピング」され、その境界として灰色の区切り線(セパレーター)が描画されます。
今回の典型例は次のようなケースです。
- 主要画面は FlyoutItem にまとめ、
FlyoutDisplayOptions="AsMultipleItems"で子要素を複数行として表示している - 「Sign Out(ログアウト)」を ShellContent として Shell 直下に追加している(または FlyoutItem と並列に配置している)
このとき、FlyoutItem の並び と 単独の ShellContent が「別セクション」として扱われやすく、結果として境界線が出ます。さらに、Sign Out がページを持たない ShellContent だと、タップ時にナビゲーションできず例外になる可能性があります。
再現しやすい XAML 構成(よくある書き方)
見た目の問題が出やすい構造を、最小化したイメージで示します(実際のページ名やルートはプロジェクトに合わせてください)。
<Shell
x:Class="MyApp.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyApp">
上記だと、フライアウトに「Home(子項目が複数)」と「Sign Out(単独)」が並び、間に区切り線が入ることがあります。さらに ShellContent Title="Sign Out" が ContentTemplate を持たないため、タップ時に No Content found for ShellContent... のような例外へつながるリスクがあります。
なぜ区切り線が出るのか:Shell の「表示単位」と「役割」を整理する
この問題を最短で理解するコツは、Shell がフライアウトを次のように扱う点です。
- FlyoutItem / TabBar / MenuItem / ShellContent など、要素の種類ごとに「表示のまとまり」を作る
FlyoutDisplayOptionsにより、子要素(ShellContent)を まとめて 1 行 にするか、複数行に展開 するかが変わる- 役割が異なる要素(ページ遷移 vs 操作)を同列に置くと、UI 上「別グループ」に見せるための区切りが入りやすい
| 宣言要素 | フライアウトでの見え方 | 向いている用途 | 注意点 |
|---|---|---|---|
| FlyoutItem | 「親」として扱われ、表示オプション次第で子項目を展開 | 主要画面群(Home / Settings など) | AsMultipleItems だと子項目が“独立行”として並ぶ |
| ShellContent | 基本は「ページへの入口」 | 実体ページを持つ画面 | ContentTemplate/ContentPage が無いとナビゲーションできず例外になり得る |
| MenuItem | 「操作メニュー」として別枠(多くの場合下部)に出る | ログアウト、キャッシュ削除など | UI 的に区切りが入るのはむしろ自然(別枠であることを示す) |
| FlyoutHeader / FlyoutFooter | フライアウトの上部/下部に自由な UI を配置できる | ユーザー情報、ログアウトボタン、バージョン表示 | 完全に自前 UI なので見た目・挙動をコントロールしやすい |
今回の「灰色の区切り線」は、バグというより “別のまとまり”として描画するためのデフォルト表現 に近い挙動です。つまり、解消の方向性は次の 2 つに整理できます。
- UI のまとまりを変える(= AsSingleItem にしてグルーピングを変える)
- そもそもログアウトを ShellContent(ページ)として扱わず、操作として配置する
解決策:FlyoutDisplayOptions を AsSingleItem にする(最短で消す)
まず一番手軽なのは、FlyoutItem 側の表示を AsSingleItem に変える方法です。
<FlyoutItem Title="Home" FlyoutDisplayOptions="AsSingleItem">
<ShellContent Title="Dashboard"
ContentTemplate="{DataTemplate local:DashboardPage}" />
<ShellContent Title="Settings"
ContentTemplate="{DataTemplate local:SettingsPage}" />
</FlyoutItem>
AsSingleItem にすると、FlyoutItem が「1 行の親」として表示され、子の ShellContent は展開(タップでサブ項目として見せる、またはタブとして扱う等)になりやすく、結果として区切り線が目立たなくなったり、出なくなったりします。
| 観点 | AsSingleItem | AsMultipleItems |
|---|---|---|
| フライアウトの見え方 | 親(Home)中心でシンプル | 子項目(Dashboard/Settings)が並び分かりやすい |
| 区切り線の出やすさ | 低い(まとまりが 1 行に寄る) | 高い(まとまりが増える) |
| 要件との相性 | 「Home だけ出ればよい」なら適合 | 「複数項目を常に見せたい」なら適合 |
ただし、要件として フライアウトに複数項目を常に並べたい(= AsMultipleItems が必須)場合、この方法は合いません。そこで次の推奨策に進みます。
解決策:AsMultipleItems のままなら「Sign Out を ShellContent にしない」(推奨)
ログアウトは多くのアプリで「ページ」ではなく「操作」です。つまり、ShellContent(ページの入口)に押し込むほど不自然になり、UI の区切り線・例外・メンテナンス性の面で不利になります。
ここからは、AsMultipleItems を維持しつつ、ログアウトを安全に実装する代表的な 3 パターンを紹介します。
パターンA:FlyoutFooter にログアウトボタンを置く(見た目を崩しにくい)
フライアウトの末尾にボタンを固定表示できるため、メニュー項目と「操作」を視覚的に分けられます。区切り線を出したくない場合も、フッター側のレイアウトを自分で作れるのが強みです。
<Shell
x:Class="MyApp.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<FlyoutItem Title="Home" FlyoutDisplayOptions="AsMultipleItems">
<ShellContent Title="Dashboard"
ContentTemplate="{DataTemplate local:DashboardPage}" />
<ShellContent Title="Settings"
ContentTemplate="{DataTemplate local:SettingsPage}" />
</FlyoutItem>
<Shell.FlyoutFooter>
<Grid Padding="16">
<Button Text="Sign Out"
Clicked="OnSignOutClicked" />
</Grid>
</Shell.FlyoutFooter>
</Shell>
コードビハインドで処理する場合は次のイメージです(MVVM で Command 化する場合は次パターンBも参照してください)。
private async void OnSignOutClicked(object sender, EventArgs e)
{
// フライアウトを閉じる(操作感が良くなる)
Shell.Current.FlyoutIsPresented = false;
// 例:確認ダイアログ
bool ok = await DisplayAlert("Sign Out", "ログアウトしますか?", "はい", "いいえ");
if (!ok) return;
// 例:トークン破棄や状態初期化(実装はアプリ設計に合わせる)
// await SecureStorage.Default.SetAsync("access_token", string.Empty);
// Preferences.Default.Clear();
// 例:ログイン画面へ戻す(ルート設計に合わせて)
await Shell.Current.GoToAsync("//Login");
}
ポイントは、ログアウトが「ページ」ではなく「処理+遷移」なので、Shell のナビゲーション構造から切り離す方がトラブルが減ることです。
パターンB:MenuItem を使って “操作” として宣言する(MVVM と相性が良い)
Shell には「ページ」以外のメニューとして MenuItem があります。Command をバインドできるため、MVVM を徹底しているプロジェクトでは扱いやすい選択肢です。
<Shell
x:Class="MyApp.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
MenuItem は多くの場合、通常のページリストとは別枠として表示されます。区切り線が入ることはありますが、それは「操作は別枠」という UX になっているため、むしろ自然です。どうしても線が不要なら、パターンAの FlyoutFooter の方がコントロールしやすいです。
パターンC:FlyoutHeader にボタンを置く(ユーザー情報と一体化したい場合)
ユーザー名やアイコンをヘッダーに表示しているアプリでは、ログアウトをヘッダーに置くと導線が分かりやすくなります。質問の例のように、ヘッダー UI の下にボタンを足すだけで実現できます。
<Shell.FlyoutHeader>
<StackLayout Padding="16" Spacing="12">
<!-- 例:ユーザー情報エリア -->
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12">
<Image Source="user.png" HeightRequest="40" WidthRequest="40" />
<StackLayout Grid.Column="1" Spacing="2">
<Label Text="{Binding UserName}" FontAttributes="Bold" />
<Label Text="{Binding UserEmail}" FontSize="12" />
</StackLayout>
</Grid>
<!-- ログアウトボタン -->
<Button Text="Sign Out" Clicked="OnSignOutClicked" />
</StackLayout>
</Shell.FlyoutHeader>
ヘッダー配置は「ページ一覧」と分離されるため、FlyoutItem と ShellContent の間に出ていた区切り線問題を根本から回避しやすい構成です。
ログアウトを安定させる実務的なチェックリスト
UI だけ直しても、ログアウトの実装が曖昧だと「ログアウトしたはずなのに戻るボタンで復帰できた」「一部キャッシュが残った」などの事故が起きます。メニュー構造を直すタイミングで、処理も一緒に整えるのがおすすめです。
| 項目 | 狙い | 実装ヒント |
|---|---|---|
| フライアウトを閉じる | タップ後の操作感を良くする | Shell.Current.FlyoutIsPresented = false; |
| 二重タップ対策 | 多重実行で状態が壊れるのを防ぐ | ボタンを一時的に無効化、またはコマンドの CanExecute 制御 |
| 認証情報の破棄 | 再利用を防ぐ | SecureStorage/Preferences の削除、メモリ上のトークン破棄 |
| 画面スタックのリセット | 戻るで復帰できないようにする | GoToAsync("//Login") のような絶対ルート遷移を検討 |
| 必要ならサーバー側の無効化 | 盗難トークンを無効化 | リフレッシュトークン失効 API を呼ぶ、デバイス登録解除など |
特に Shell を使うアプリでは、ログアウト後に “戻る” で以前のページに戻れてしまうケースが起きがちです。ログアウト後の画面遷移は、スタックが残らない形(絶対ルート遷移や、認証状態に応じた初期ルート切り替え)を意識すると安全です。
どうしても「Sign Out 画面」を作りたい場合の正しい ShellContent
アプリの要件として「ログアウト前に注意事項を表示したい」「アカウント切り替え UI を出したい」など、ログアウト専用ページが必要なこともあります。その場合は ShellContent として置いても構いませんが、必ず ContentPage(ContentTemplate)を持たせるのが前提です。
<ShellContent Title="Sign Out"
Route="signout"
ContentTemplate="{DataTemplate local:SignOutPage}" />
SignOutPage 側では、ボタン押下でログアウト処理を実行し、完了後にログイン画面へ遷移させます。ShellContent を “操作” として使うのではなく、“ページ” として完結させると、責務が明確になります。
区切り線が消えない・挙動が揃わないときの追加チェック
同じ XAML でも、スタイルやテンプレート、プラットフォーム差で見え方が変わることがあります。最後に、実務でハマりやすい確認ポイントをまとめます。
- ShellContent が混在していないか:Shell 直下に複数の ShellContent を置くと、セクションが増え区切りが入りやすくなります。意図が「ページ一覧」なのか「操作」なのかで置き場所を分けます。
- FlyoutItem のまとまりを見直す:Home/Settings などの“主要導線”は FlyoutItem に寄せ、例外的な項目だけが外に出ないようにします。
- 例外が出ていないか:タップ時の例外が出ている場合、見た目の問題より先に
ContentTemplateの未設定を疑います。 - ヘッダー/フッターの余白:FlyoutHeader/FlyoutFooter のレイアウトが大きすぎると、メニューがスクロールしづらくなります。Padding と最小限の UI を意識します。
まとめ:区切り線の正体は「グルーピング」、ログアウトは “操作” として扱う
FlyoutItem と ShellContent の間に出る灰色の区切り線は、Shell がメニューを「まとまり」で描画している結果です。最短で消したいなら FlyoutDisplayOptions="AsSingleItem" が効きますが、AsMultipleItems を維持したいなら、ログアウトを ShellContent で表現しないのが安全です。
おすすめは FlyoutFooter / FlyoutHeader にボタンを置く、または MenuItem を使って Command として宣言すること。ページ遷移の責務と、操作の責務を分けるだけで、見た目・例外・保守性をまとめて改善できます。

コメント