.NET MAUIでページ表示後に処理を実行する方法:OnAppearing/Loaded/SizeChangedの使い分け

.NET MAUIでNavigation.PushAsyncした直後に重い処理を走らせると、画面が固まったりActivityIndicatorが見えないことがあります。OnAppearingよりも“ページ表示後”に寄せたいとき、使えるイベントと限界、現場で破綇しにくい実装パターンを整理します。

目次

.NET MAUIで「ページが表示された直後」に確実に走る標準イベントはあるのか

先に結論から言うと、現時点の.NET MAUIには「ユーザーに見えるように描画が完了した直後」をクロスプラットフォームで保証してくれる、分かりやすいページライフサイクルイベントは用意されていません。質問でよく期待される「レンダリング完了」「表示完了」のような合図は、標準APIとしてはまだ弱い(もしくは意図的に用意されていない)という理解が現実的です。

特に混乱しやすいのが OnAppearing() で、名前の印象に反して「画面に見えてから」ではなく、あくまで“表示に入るタイミング(表示準備が整ったタイミング)”で呼ばれます。ナビゲーションのアニメーション、レイアウト計測、バインディング適用、画像デコードなどが絡むため、OnAppearingの時点でユーザーが視認できる状態になっているとは限りません。

なぜ「表示直後」を保証するのが難しいのか(MAUIの事情)

「表示された直後」は人間には分かりやすい概念ですが、フレームワーク側で統一するのは意外に難しいです。理由はざっくり次の通りです。

  • 描画は段階的:ページの生成 → ビジュアルツリーへの追加 → 計測(Measure)→ 配置(Arrange)→ 描画、と段階があり、途中で何度もやり直しが発生します。
  • プラットフォーム差:iOS/Android/Windowsでは「表示」の定義やナビゲーションアニメーションの完了タイミングが異なります。
  • ナビゲーションはアニメーションを伴う:画面が“完全に見える”のはアニメーション終了後ですが、イベントはその前に発火することがあります(特にiOS/Mac Catalystで顕著)。
  • 重い処理がUIスレッドを塞ぐ:OnAppearingなどで同期処理・重い処理をすると、UI更新自体が遅れ、「スピナーが出る前に処理が走る」「画面が固まる」が起きます。

つまり「表示直後」を狙うには、目的に応じて “十分に遅いタイミング” を自前で作る、という発想が必要になります。

まず押さえる:よく使うイベントと「何が保証されるか」

タイミング/イベント発火の目安保証できること向いている用途注意点
コンストラクタページ生成時オブジェクトが存在するDI注入・イベント購読・初期値設定UIはまだ“表示”と無関係
OnAppearing / Appearing表示に入る直前〜直後(保証なし)ページが表示対象になった毎回の表示更新、軽い処理“描画完了後”ではない
Loaded(VisualElement.Loaded)要素が構築され、プラットフォームのビジュアルツリーに追加された時ツリーに載った初回の初期化、初回だけ走らせる処理計測前に発火することがある(サイズ保証なし)
SizeChangedサイズが変わった時少なくとも一度は計測・配置が走った可能性が高い幅/高さが必要な処理、スクロール位置調整何度も発火する(回転・リサイズ等)
NavigatedTo / OnNavigatedToページにナビゲートされた直後「ナビゲーション完了相当」を通知ナビゲーションを起点にする処理iOS/Mac Catalystではアニメーション完了前に発火し得る

表の通り、Loadedは“ツリーに載った”を検知できる一方で、サイズ確定や描画完了は保証しません。そしてNavigatedToも「ユーザーに見えた瞬間」と一致しないケースがあり得ます。だからこそ、用途別の組み合わせが重要です。

代替案の本命:VisualElement.Loadedで「ロード完了寄り」を取る

.NET MAUIには VisualElement.Loaded イベントがあり、要素が構築されてプラットフォームのビジュアルツリーに追加されたときに発火します。ContentPageもVisualElement系なので、ページ自身(またはルート要素)で購読できます。

最小構成:ページでLoadedを1回だけ受ける

public partial class DetailPage : ContentPage
{
    private bool _initialized;


public DetailPage()
{
    InitializeComponent();

    // 初回だけ走らせたいならLoadedで購読→解除が鉄板
    Loaded += OnLoaded;
}

private void OnLoaded(object? sender, EventArgs e)
{
    if (_initialized) return;
    _initialized = true;

    Loaded -= OnLoaded;

    // ここで「ロード後にやりたい処理」
    StartLazyLoad();
}

private void StartLazyLoad()
{
    // 例:軽い初期化、一覧のScrollToなど
}


}

ポイントは「Loadedは初回だけでいいのか、表示のたびに必要なのか」を最初に決めることです。初回だけなら解除しておくと、ページ再利用やテンプレート再適用のようなケースでも意図せず複数回走る事故が減ります。

注意:Loadedは「サイズ確定」ではない

Microsoft LearnのLoadedの備考にもある通り、Loadedは計測(Measure)より前に発火し得るため、Width/Heightが欲しい処理には不十分なことがあります。

例えば次のようなケースはLoaded単体では事故りやすいです。

  • 初回表示で「特定位置へスクロール」するが、まだレイアウトが固まっていない
  • 要素のWidth/Heightを使って計算したい(Canvas、画像のトリミング、座標計算など)
  • ActivityIndicatorを「見せてから」重い処理をしたいが、スピナー表示が間に合わない

レイアウト確定が必要なら:Loaded+SizeChangedで“1回目のサイズ確定”を拾う

「ツリーに載った」だけでなく、幅・高さが確定した後に処理したい場合は、Loadedを起点にしてSizeChangedを1回だけ受けるのが実務で安定します。

public partial class ChartPage : ContentPage
{
    private bool _layoutHandled;

    public ChartPage()
    {
        InitializeComponent();
        Loaded += OnPageLoaded;
    }

    private void OnPageLoaded(object? sender, EventArgs e)
    {
        // Loadedは初回だけでOKなら解除
        Loaded -= OnPageLoaded;

        // サイズ確定を待つ
        SizeChanged += OnSizeChangedOnce;
    }

    private void OnSizeChangedOnce(object? sender, EventArgs e)
    {
        // サイズが未確定っぽいときは早期return(環境差対策)
        if (Width <= 0 || Height <= 0) return;

        if (_layoutHandled) return;
        _layoutHandled = true;

        SizeChanged -= OnSizeChangedOnce;

        // ここでサイズ依存の処理
        InitializeChartLayout(width: Width, height: Height);
    }

    private void InitializeChartLayout(double width, double height)
    {
        // 例:サイズに応じた描画、ScrollTo、画像の計算など
    }
}

SizeChangedは「サイズが変わるたび」なので、回転・マルチウィンドウ・デスクトップでのリサイズなどでも再び発火します。初回だけにしたいなら、上のようにフラグ+解除で制御します。

「表示されてから」を体感的に改善する実務ワークアラウンド

イベントだけで完全な描画完了を掴めない以上、ユーザー体験を崩さないための“やり方”が重要になります。ここでは、タイマー頼みの力技を避けつつ、現場で効くパターンをまとめます。

やりたいこと推奨パターン狙い実装のコツ
初回表示後にデータを読みたいLoadedで開始(軽い処理)ページ生成直後の詰まりを回避Loadedは1回だけで解除
幅/高さが必要な初期化をしたいLoaded→SizeChanged(1回)レイアウト確定後に寄せるWidth/Height>0を確認して解除
スピナーを確実に見せてから重い処理表示→UIに“1回譲る”→処理開始まず描画を通すTask.YieldやDispatcherで次のタイミングへ
ページ再表示ごとに更新したいOnAppearing / NavigatedToで更新毎回の更新を担保重い処理は非同期+キャンセル

「UIに1回譲る」:Task.Yieldで体感を改善する

「タイマーで数十ms待つ」の代わりに、まずはUIスレッドに制御を返して描画チャンスを作る、という発想が有効です。具体的には、OnAppearingから非同期メソッドを呼び、最初に await Task.Yield() を挟みます。

public partial class StartPage : ContentPage
{
    private CancellationTokenSource? _cts;

    protected override void OnAppearing()
    {
        base.OnAppearing();

        // 表示のたびに走る処理(重いなら非同期+キャンセル)
        _cts?.Cancel();
        _cts = new CancellationTokenSource();

        _ = RunAfterAppearingAsync(_cts.Token);
    }

    protected override void OnDisappearing()
    {
        _cts?.Cancel();
        base.OnDisappearing();
    }

    private async Task RunAfterAppearingAsync(CancellationToken token)
    {
        // まずUIに描画チャンスを与える
        await Task.Yield();

        // ここでスピナーを見せたいなら、Visible切り替え→Yield→処理開始が効く
        LoadingIndicator.IsVisible = true;
        LoadingIndicator.IsRunning = true;

        await Task.Yield();

        try
        {
            await LoadDataAsync(token);
        }
        catch (OperationCanceledException)
        {
            // 画面遷移などでキャンセルされた場合
        }
        finally
        {
            LoadingIndicator.IsRunning = false;
            LoadingIndicator.IsVisible = false;
        }
    }

    private async Task LoadDataAsync(CancellationToken token)
    {
        // 例:API呼び出し、DB読み込み、画像準備など
        await Task.Delay(1, token); // サンプル(実際はI/O処理)
    }
}

Task.Yieldは「一定時間待つ」ではなく、「今の処理を一旦終えて次のメッセージループへ回す」イメージなので、タイマーよりも意図が明確で副作用が少ないことが多いです。それでもプラットフォームや負荷状況で完全には保証できない点は理解しておきましょう。

どうしても必要なら短いDelayを使う(最終手段)

LoadedやSizeChanged、Task.Yieldを組み合わせても「スピナーが見えない」「初回だけ固まる」が残ることがあります。これは、画像デコードや初回バインディングのコストがUIスレッドを占有し、描画の機会がさらに後ろに押されるためです。その場合だけ、50〜100ms程度のごく短いDelayを入れるのは、現場では珍しくありません(ただし依存しすぎないことが重要です)。

NavigatedTo / OnNavigatedToは使える?(ただし万能ではない)

.NET MAUIのPageには NavigatedTo イベントや OnNavigatedTo が用意されています。ナビゲーションを起点に処理を整理したい場合には有用です。

public partial class ListPage : ContentPage
{
    protected override void OnNavigatedTo(NavigatedToEventArgs args)
    {
        base.OnNavigatedTo(args);

        // ナビゲーション直後にやりたい処理(例:選択状態の復元など)
        RestoreSelection();
    }

    private void RestoreSelection()
    {
        // 例:前回の選択を復元
    }
}

ただし、NavigationPageのドキュメントにもある通り、iOS/Mac Catalystではナビゲーションイベントがネイティブアニメーションの完了前に発火する場合があるため、「ユーザーに見えてから」を厳密に担保する用途には向きません。

Shellアプリのライフサイクルも「表示完了」を保証するわけではない

Shellを使っている場合、ナビゲーション時にどのオブジェクトがAppearing/Disappearingを出すか、といった整理が必要になります。Microsoft LearnにもShellナビゲーションに伴うAppearing/Disappearingの動きがまとめられています。

ただし、ここでのAppearingもやはり「描画完了後」を保証するものではありません。Shellであっても、基本戦略は同じで、LoadedやSizeChanged、非同期化などで“十分に遅いタイミング”を作ります。

Loadedにも落とし穴がある:発火の回数・ペアリング問題

Loadedは便利ですが、常に「1回だけ」「必ずUnloadedと対になる」とは限りません。ControlTemplateの再適用、要素の差し替え、プラットフォーム差などで、Loaded/Unloadedの発火が想定とズレることがある、という報告もあります。実装側では次の対策が無難です。

  • イベントは基本“購読したら解除”:ページ破棄や再利用時の多重実行・メモリリークを防ぐ。
  • 初回だけやりたい処理はフラグでガード:Loadedが複数回起きても破綻しない。
  • 「表示のたびに必要」ならOnAppearing/NavigatedToへ寄せる:Loadedに“毎回”を期待しない。

目的別:実装パターンの選び方(迷ったときの指針)

最後に「結局どれを使うべき?」を目的別に整理します。

目的おすすめ理由
初回だけ初期化したい(DI後のセットアップなど)Loaded(1回で解除)ツリーに載った後に走らせられ、コンストラクタより安全
初回表示でスクロール位置を合わせたいLoaded→SizeChanged(1回)レイアウト確定後の方がScrollToが安定
ページ表示のたびにデータ更新したいOnAppearing または NavigatedToページ再表示がトリガーになる
とにかく「まずUIを出してから」重い処理をしたいOnAppearing内で非同期化→Task.YieldUIに描画チャンスを与え、体感を改善しやすい
どうしても“完全に見えた後”が必要プラットフォーム固有実装(上級)ネイティブのViewDidAppear等に寄せるしかない

まとめ:.NET MAUIでは「表示完了イベント」はない前提で設計する

.NET MAUIで「ページが表示された直後に処理を実行したい」と思ったとき、最初に受け入れるべき現実は、“描画完了を保証する標準イベントは(まだ)用意されていない”という点です。

その上で、

  • ツリーに載った後に寄せたい → Loaded
  • 幅・高さなどレイアウト確定が必要 → Loaded+SizeChanged
  • 体感を良くする(UIを先に見せる) → 非同期化+Task.Yield

という形で、目的に合わせてイベントを組み合わせるのが、現時点での最も実務的で再現性の高い解決策です。

この記事を書いた人

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

コメント

コメントする

目次