.NET MAUI WindowsでFlyoutPageのIsPresentedが更新されない原因と解決策(NavigationView PaneClosed)

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 / PropertyChangedIsPresentedの値
ハンバーガーボタンで開く開く発火する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 = falseMAUI側の状態を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がnullLoaded後に取得する(または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版の体験が安定します。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次