Windows上の.NET MAUIでFlyoutPageを使うと、Flyoutを外側クリックで閉じたときにIsPresentedが変化せず、開閉状態に合わせたUI制御(ポップアップ位置など)が破綻することがあります。原因と回避策を具体的なコード付きで整理します。
起きている問題:FlyoutPageのIsPresentedが「外側クリック」で戻らない
.NET MAUIのFlyoutPageは、左側にFlyout(メニュー)を持ち、ハンバーガーボタン(≡)などで開閉できるUIを作れます。ところがWindows(WinUI)でFlyoutPageを使うと、Flyoutを開いた状態(IsPresented = true)からコンテンツ領域をクリックして閉じたときに、見た目は閉じているのにIsPresentedがfalseに戻らないことがあります。
この状態になると、Flyoutの開閉に連動して位置やサイズを変えたいUI(例:Flyoutの横に出すポップアップ、チップ、ツールパネル、フローティングボタンなど)が「開いている扱い」のまま計算され、表示がずれたり、意図せず画面外にはみ出したりします。
この記事の対象:ShellではなくFlyoutPageを使っているケース
MAUIにはShellにもFlyout(いわゆるハンバーガーメニュー)がありますが、今回の話はShellではなくFlyoutPageを使っている場合に発生しやすい問題です。
- Shell:
FlyoutIsPresentedや関連イベントで状態管理しやすい構造 - FlyoutPage:
IsPresentedを起点に開閉を追いかけるのが基本
「Flyoutの開閉状態に応じて、Flyout横のポップアップ位置を変えたい」という要件はFlyoutPageでも十分実現できますが、Windowsだけ開閉検知が欠けると破綻します。ここを補正するのが本記事のゴールです。
症状を整理:期待値と実際の挙動の差
まずは「どの操作で、何が発火し、どこが更新されないか」を切り分けると、原因と対策が一気に見えます。
| 操作 | 見た目(Flyout) | IsPresentedChanged / PropertyChanged | IsPresentedの値 |
|---|---|---|---|
| ハンバーガーボタンで開く | 開く | 発火する | trueになる |
| ハンバーガーボタンで閉じる | 閉じる | 発火する | falseになる |
| Flyoutを開いたまま外側(コンテンツ)をクリック | 閉じる | 発火しない場合がある | trueのまま残る場合がある |
ポイントは3行目で、UIは閉じたのにMAUI側の状態(IsPresented)が更新されないため、開閉検知の起点にしていたイベントやバインディングが動かなくなります。
影響が出やすいケース:左コンパクト表示とポップアップ配置
Windowsの左コンパクト表示(いわゆる「アイコンだけ細く残り、開くと幅が広がる」タイプ)では、開閉によってFlyoutの占有幅が変わります。そこでポップアップをFlyoutの横に出す場合、次のようなロジックになりがちです。
- Flyoutが開いている(
IsPresented == true)なら、ポップアップのX座標を「Flyoutの開いた幅」だけ右にずらす - Flyoutが閉じている(
IsPresented == false)なら、ポップアップのX座標を「コンパクト幅」または「0」基準で計算する
この判断が崩れると、ポップアップの基準点が古いまま残り、ユーザー視点では「閉じたのに、開いている位置に出る」ように見えます。
簡易再現:FlyoutPageを最小構成で作って挙動を確認する
切り分けのために、まずはFlyoutPage単体で「IsPresentedが変わる/変わらない」を確認できる状態を作るのがおすすめです。以下は最小構成の例です(UIは簡素でOKです)。
<FlyoutPage
x:Class="YourApp.YourFlyoutPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<FlyoutPage.Flyout>
<ContentPage Title="Menu">
<VerticalStackLayout Padding="16">
<Label Text="Flyout (Menu)" FontSize="20" />
<Button Text="メニュー項目A" />
<Button Text="メニュー項目B" />
</VerticalStackLayout>
</ContentPage>
</FlyoutPage.Flyout>
<FlyoutPage.Detail>
<ContentPage Title="Detail">
<VerticalStackLayout Padding="16">
<Label Text="Detail (Content)" FontSize="20" />
<Label Text="Flyoutを開いた後、ここ(コンテンツ)をクリックして閉じてみてください。" />
</VerticalStackLayout>
</ContentPage>
</FlyoutPage.Detail>
</FlyoutPage>
この状態で、Flyoutを開いた後にコンテンツ側をクリックして閉じ、ログでIsPresentedが戻るかどうかを確認します。戻らない場合、今回のWindows固有挙動に該当している可能性が高いです。
原因:Windows(WinUI)では内部のNavigationViewだけが閉じ、IsPresentedに反映されないことがある
Windows上のFlyoutPageは、内部的にWinUIのNavigationView(ペイン付きのナビゲーションコンポーネント)に近い仕組みでFlyoutの開閉を実現しています。外側クリックで閉じる操作は、WinUI側の「ペインのポップオーバーが閉じる」処理としては成立しますが、MAUIのFlyoutPage.IsPresentedプロパティ更新まで伝播しないケースがあります。
結果として、MAUI側では
IsPresentedがfalseにならないIsPresentedChanged(またはPropertyChanged)が発火しない
という状態になり、「外側クリックで閉じた」パターンだけ検知できません。
対策の方針:WindowsだけWinUIイベントを拾い、IsPresentedを自前で同期する
MAUI側のイベントが上がってこないなら、下層(WinUI)のイベントをフックして、MAUI側の状態を補正するのが実用的です。WindowsではNavigationViewにPaneClosedイベントがあるため、これを拾って明示的にIsPresented = falseをセットします。
| やること | 狙い | ポイント |
|---|---|---|
WindowsでNavigationView.PaneClosedを購読 | 外側クリックで閉じたタイミングを確実に検出 | Handler生成後(Loaded後)に取得する |
PaneClosedでIsPresented = false | MAUI側の状態をWinUIに合わせる | 既にfalseなら二重更新しない |
PropertyChangedで開閉処理を一本化 | ポップアップ座標計算を安定化 | 「開いた/閉じた」双方で同じメソッドを呼ぶ |
実装例:PaneClosedをフックしてIsPresentedを補正する(.NET MAUI / Windows)
以下はFlyoutPage側で、IsPresentedの変化を監視しつつ、WindowsではNavigationView.PaneClosedをフックしてIsPresentedを確実にfalseへ戻す例です。
ポイント:
- WindowsだけプラットフォームAPIに触れる(
#if WINDOWS) Handlerができる前にPlatformViewを触るとnullになるため、Loaded後に取得する- ページを再利用する設計なら、イベント多重購読を避けるために解除も検討する
using System.ComponentModel;
using System.Diagnostics;
#if WINDOWS
using Microsoft.UI.Xaml.Controls;
#endif
namespace YourApp;
public partial class YourFlyoutPage : FlyoutPage
{
#if WINDOWS
private NavigationView? _navView;
#endif
public YourFlyoutPage()
{
InitializeComponent();
// IsPresented の変更を監視(開閉処理の起点を一本化)
PropertyChanged += OnFlyoutPagePropertyChanged;
#if WINDOWS
Loaded += OnLoaded;
#endif
}
#if WINDOWS
private void OnLoaded(object? sender, EventArgs e)
{
Loaded -= OnLoaded;
// Handler / PlatformView が確実に作られてから取得する
if (Handler?.PlatformView is NavigationView navView)
{
_navView = navView;
// Flyoutの外側クリックなどでWinUI側のペインが閉じたら、
// MAUI側の IsPresented も確実に false へ戻す
_navView.PaneClosed += (_, _) =>
{
if (IsPresented)
IsPresented = false;
};
}
}
#endif
private void OnFlyoutPagePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(IsPresented))
{
Debug.WriteLine($"IsPresented = {IsPresented}");
// ここに「開いた/閉じた」に応じた処理を集約する
UpdatePopupPlacement();
}
}
private void UpdatePopupPlacement()
{
// Flyoutの開閉に応じてポップアップ位置を計算して更新する
// 実装例は後述
}
}
この補正により、ハンバーガーボタンで閉じた場合だけでなく、外側クリックで閉じた場合も最終的にIsPresentedがfalseへ更新され、PropertyChanged経由で確実に「閉じた」ことを検出できるようになります。
(任意)イベント解除も入れて堅牢化したい場合
ページの再生成やナビゲーション構造によっては、イベント購読が積み重なって同じ処理が複数回走ることがあります。気になる場合は、OnAppearing/OnDisappearingで購読と解除を管理すると安定します(以下は一例です)。
#if WINDOWS
protected override void OnAppearing()
{
base.OnAppearing();
if (_navView is null && Handler?.PlatformView is NavigationView navView)
{
_navView = navView;
_navView.PaneClosed += OnPaneClosed;
}
}
protected override void OnDisappearing()
{
if (_navView is not null)
{
_navView.PaneClosed -= OnPaneClosed;
_navView = null;
}
base.OnDisappearing();
}
private void OnPaneClosed(NavigationView sender, object args)
{
if (IsPresented)
IsPresented = false;
}
#endif
ポップアップ位置制御への落とし込み:Flyoutの幅を基準にX座標を決める
目的が「Flyoutの横にポップアップを出す」なら、最終的にはFlyoutの占有幅を基準にするのが安定します。特にWindowsの左コンパクトでは、閉じたときに完全に0になるのではなく、コンパクト幅が残ることが多い点が重要です。
| 状態 | Flyoutが占有する幅のイメージ | Windows(NavigationView)で参照しやすい値 |
|---|---|---|
| 閉じている | コンパクト幅(アイコン列など)が残る場合がある | CompactPaneLength |
| 開いている | メニューが展開され、幅が広がる | OpenPaneLength |
Windows限定にはなりますが、Paneの幅をNavigationViewから取得できるなら、ハードコードせずに「今の幅」を使えるため、テーマ変更やDPI、ユーザー設定によるズレにも強くなります。
private double GetFlyoutOffsetX()
{
#if WINDOWS
if (_navView is not null)
{
// 左コンパクトを想定:
// 閉じているときは CompactPaneLength、
// 開いているときは OpenPaneLength を基準にする
return IsPresented ? _navView.OpenPaneLength : _navView.CompactPaneLength;
}
#endif
// Windows以外、または取得できない場合は既定値(必要に応じて調整)
return IsPresented ? 320 : 0;
}
あとは、このオフセットを使ってポップアップのX座標を決めます。実装はポップアップの方式(CommunityToolkitのPopup、AbsoluteLayout上のView、独自のOverlayなど)によって変わりますが、考え方は共通です。
- Flyoutの状態(
IsPresented)が変わったら、必ず再計算する - 再計算したX座標をポップアップに反映する
例えばAbsoluteLayoutで自前ポップアップViewを載せているなら、次のような形で更新できます。
// 例:AbsoluteLayout上のポップアップViewを移動するイメージ
private void UpdatePopupPlacement()
{
var flyoutOffsetX = GetFlyoutOffsetX();
// Flyoutの右端から少しマージンを取る
var x = flyoutOffsetX + 12;
// Y座標は任意(例:上から80px)
var y = 80;
// popupView は AbsoluteLayout 上に配置している前提
AbsoluteLayout.SetLayoutBounds(popupView, new Rect(x, y, popupView.Width, popupView.Height));
}
ここで重要なのは、更新トリガーをFlyoutPageのPropertyChanged(IsPresented)に寄せている点です。Windowsで外側クリックだけ検知できない問題を補正できれば、開閉方法に依存せず、いつでも同じ座標計算が走ります。
実装時の注意点:Handler取得タイミングと多重購読
「動くはずなのに動かない」ケースの多くは、HandlerやPlatformViewを取るタイミングに起因します。チェック観点をまとめます。
| チェック項目 | ありがちな失敗 | 対処 |
|---|---|---|
| PlatformView取得タイミング | コンストラクタ直後にHandlerがnull | Loaded後に取得する(またはHandler生成後フック) |
| キャスト対象 | Handler.PlatformViewの型が想定と違う | is NavigationViewで安全に判定してから購読 |
| イベントの多重購読 | ページ再表示で購読が増え、処理が複数回走る | 解除する/購読済み判定を入れる |
| UIスレッド | バックグラウンドからUI更新して例外 | 必要ならMainThread.BeginInvokeOnMainThreadで反映 |
トラブルシューティング:それでも検知できないときの確認ポイント
補正を入れても「まだ閉じた扱いにならない」ときは、次を順に確認すると切り分けが早いです。
| 症状 | 原因の候補 | 確認・対処 |
|---|---|---|
PaneClosedが一度も来ない | NavigationViewにフックできていない | Loaded内でHandler?.PlatformViewの型をログ出力して確認 |
PaneClosedは来るがIsPresentedが変わらない | IsPresentedが既にfalse、または更新が抑止されている | ハンバーガーで開閉したときは変わるか/ガード条件を見直す |
| ポップアップだけズレる | 幅の参照値が固定 / 取得できていない | OpenPaneLength/CompactPaneLengthをログし、実測値で補正 |
| 常時表示(Split系)で想定が崩れる | Flyoutが常に表示され、IsPresentedの意味が変わる | 常時表示なら「常に開いている扱い」で座標計算する設計に寄せる |
より安定させる小技:座標計算をメソッド化して「必ず同じ経路で更新」する
Flyoutに連動するUIは、最終的に「状態変化 → 再計算 → 反映」を一本化すると崩れにくくなります。おすすめは次の3点です。
- Flyoutの開閉検知は
PropertyChanged(IsPresented)に集約する - 座標計算は
GetFlyoutOffsetX()など純粋メソッド寄りにしてテストしやすくする - ポップアップ表示/非表示も同じメソッド内で決め、状態の二重管理を避ける
「イベントが来たから位置をずらす」ではなく、「今の状態から正しい位置を計算する」に寄せると、開閉イベントの取りこぼしがあっても回復しやすくなります(今回のようなWindows固有挙動に強い設計です)。
まとめ:WindowsのFlyoutPageはWinUIイベントで補正し、IsPresentedを信頼できる状態に戻す
- Windows + FlyoutPageでは、外側クリックで閉じた際に
IsPresentedが更新されないケースがある - WinUIの
NavigationView.PaneClosedをフックし、IsPresented = falseを明示的にセットする - 補正できれば、
PropertyChanged(IsPresented)を起点にポップアップ位置を一貫して再計算できる - 左コンパクト表示なら、Windowsでは
OpenPaneLength/CompactPaneLengthを使うとハードコードより安全
Flyoutの開閉方法(ボタン/外側クリック)に依存せず「今どう見えているか」に追従できるようになれば、UIの一貫性が上がり、Windows版の体験が安定します。

コメント