.NET MAUI の画面を「初期状態に戻す」処理は、ログアウトや権限変更時に必須です。これまで Application.Current.MainPage を差し替えていた人が、.NET 9 の CreateWindow() 構成で迷いがち。この記事では Window 起点の考え方と、安全にページをリセットする実装パターンを解説します。
.NET MAUI(.NET 9)で「ページ(画面)をリセット」したくなる典型シーン
「画面をリセットしたい」と言っても、現場で求められる要件はだいたい次のどれかです。目的を先に整理しておくと、実装の迷いが減ります。
| よくある場面 | なぜリセットが必要か | 実装でやりたいこと |
|---|---|---|
| ログアウト | 戻る操作で前ユーザーの画面に戻れてしまう事故を防ぐ | ナビゲーション履歴を破棄し、ログイン画面(またはトップ)に戻す |
| 権限・ロール切り替え | メニュー構成や表示可否が変わる | Shell を作り直す/ルートを作り直す |
| 初期セットアップ完了 | セットアップフローの戻りを消したい | 「セットアップ用のルート」から「通常ルート」に切り替える |
| 致命的な状態不整合からの復帰 | 画面スタックが壊れた・複数モーダルが積み上がった | ルートページを再生成し、UI を作り直す |
従来の Xamarin.Forms や .NET MAUI の初期(.NET 7/8 で単一ウィンドウ運用が前提になっていたケース)では、次のように MainPage を差し替えるのが「手っ取り早いリセット」でした。
Application.Current.MainPage = new AppShell();
ところが .NET 9 のテンプレートや、マルチウィンドウを前提にした構成では「アプリの起点が Window」になります。ここを理解すると、リセットのやり方が自然に見えてきます。
なぜ .NET 9 では Application.Current.MainPage の差し替えだけだと不安になるのか
.NET MAUI は内部的に「アプリケーション(Application)」が「ウィンドウ(Window)」を作り、ウィンドウが「ルートページ(Page)」を持つ構造になっています。CreateWindow() をオーバーライドする構成では、この関係がコード上でも明確になります。
| 概念 | 役割 | リセット観点で重要な点 |
|---|---|---|
| Application | アプリ全体のライフサイクルと設定の中心 | 複数 Window を持てる。単一の MainPage 前提が崩れる |
| Window | 表示単位(デスクトップなら本当に複数開ける) | Page が「そのウィンドウのルート」。ここを差し替えると画面が作り直せる |
| Page(AppShell / MainPage など) | 実際の UI ルート | ナビゲーションスタック、Shell のルート、メニュー、Tab 構成などに直結 |
つまり、.NET 9 の「Window 起点」構成で “画面リセット” をしたいなら、狙うべき差し替え先は Application.MainPage ではなく、対象の Window の Page です。
結論:生成した Window インスタンスの Page を差し替える
受理された回答の要点は非常にシンプルです。
CreateWindow()で作ったWindowを保持する- リセット時に
WindowInstance.Page = new XXXPage()のようにルートページを置き換える
最小構成の実装例
まずは「仕組みが分かる」最小例です。ログアウトなどのタイミングで呼べる Reset() を用意します。
public partial class App : Application
{
public static Window WindowInstance { get; private set; }
protected override Window CreateWindow(IActivationState? activationState)
{
WindowInstance = new Window(new AppShell());
return WindowInstance;
}
public static void Reset()
{
// 例:ログアウト後にトップへ戻す、など
WindowInstance.Page = new MainPage(); // または new AppShell()
}
}
これで、ウィンドウのルートページが差し替わるため、ナビゲーション履歴も含めて UI を「作り直す」動きになります。従来の MainPage 差し替えと近い感覚で使えます。
実務向け:UI スレッドで安全に差し替える
ViewModel からログアウト処理を呼ぶ、API 応答後にリセットする、といった現場あるあるでは、呼び出しスレッドが UI スレッドとは限りません。Window.Page の差し替えは UI スレッドで行うのが安全です。
using Microsoft.Maui.ApplicationModel;
public partial class App : Application
{
public static Window? WindowInstance { get; private set; }
protected override Window CreateWindow(IActivationState? activationState)
{
var window = new Window(new AppShell());
WindowInstance = window;
return window;
}
public static void ResetTo(Page newRoot)
{
MainThread.BeginInvokeOnMainThread(() =>
{
var window = WindowInstance ?? Application.Current?.Windows?.FirstOrDefault();
if (window == null)
return;
window.Page = newRoot;
});
}
}
ポイントは次の 3 点です。
- UI スレッドで実行:例外回避と安定性のため
- null 対策:テストや特殊な起動経路で
WindowInstanceが未セットの可能性に備える - 引数でルートを受け取る:ログイン画面へ戻す、Shell を作り直すなど用途に合わせやすい
Dispatcher を使う選択肢もある
UI スレッドへの切り替えは MainThread 以外にも手段があります。アプリ側の実装方針(テストのしやすさ、依存の少なさ)に合わせて選んでください。
| 方法 | 特徴 | 向いているケース |
|---|---|---|
MainThread.BeginInvokeOnMainThread | 呼び出しが簡単。どこからでも使いやすい | ViewModel から呼ぶ、サービス層から呼ぶなど、UI 層から離れた場所 |
Application.Current.Dispatcher.Dispatch | Dispatcher を中心に統一できる | アプリ全体で Dispatcher を使う方針、UI 周りの処理を統一したい |
ページや要素の Dispatcher | 対象の UI に紐づく Dispatcher を使える | 「今表示しているページ」での UI 更新を確実にしたい |
「リセット=アプリを作り直す」ではない:状態の残り方を理解する
Window.Page を差し替えると UI は作り直せますが、アプリの DI コンテナ(MauiProgram で登録したシングルトンなど)やプロセス自体が再起動するわけではありません。ここを勘違いすると「リセットしたのに状態が残る」現象にハマります。
| リセットで消えるもの | リセットしても残るもの | 対策の考え方 |
|---|---|---|
| 画面ツリー、ナビゲーション履歴、モーダルスタック(基本的に) | Singleton サービス、静的フィールド、キャッシュ、HTTP クライアントの状態 | ログアウト時は「セキュアストレージ」「トークン」「ユーザー情報キャッシュ」を明示的に破棄する |
| Shell のメニュー構成(Shell を作り直した場合) | イベント購読(解除しない限り残ることがある) | 画面側で購読したイベントは OnDisappearing 等で解除する |
| BindingContext(ページ生成し直しでリセット) | バックグラウンドタスク、タイマー | ログアウト時に停止する、CancellationToken で止める |
「UI の初期化」と「アプリ内状態の初期化」は別物です。リセット処理は、UI の再生成とセットで “残すべきでない状態” を消すところまで設計すると、再ログイン後の不具合が激減します。
実務で使えるリセット設計パターン
パターンA:単一ウィンドウ前提で WindowInstance を静的に保持
スマホ中心・シングルウィンドウ前提であれば、静的プロパティ方式が最短です。テンプレートに近く、理解もしやすいのがメリットです。
- メリット:実装が簡単、呼び出し側が楽
- デメリット:将来マルチウィンドウにしたいときに設計変更が必要
パターンB:複数 Window を想定して対象 Window を選んで差し替える
Windows / Mac で「複数ウィンドウ」を使う可能性があるなら、静的に 1 個だけ持つ方式は注意が必要です。用途によっては Application.Current.Windows から対象ウィンドウを選ぶ設計の方が事故が減ります。
まずは割り切った例として、「先頭のウィンドウを対象にする」パターンです。
public static void ResetFirstWindowTo(Page newRoot)
{
MainThread.BeginInvokeOnMainThread(() =>
{
var window = Application.Current?.Windows?.FirstOrDefault();
if (window == null)
return;
window.Page = newRoot;
});
}
「どの Window が対象か」を確実にしたい場合は、ウィンドウ生成時に ID を持たせたり、ページ側から “自分が属する Window” を参照してリセット対象を決めたりします。簡単な運用ルールを決めるだけでも、マルチウィンドウ時の事故(別ウィンドウが突然ログアウト画面になる等)を避けられます。
| 対象 Window の決め方 | 向いているケース | 注意点 |
|---|---|---|
Application.Current.Windows.FirstOrDefault() | とにかく簡単にしたい。実質 1 Window 運用 | 複数 Window があると意図しないものを対象にする可能性 |
| 生成時に辞書で保持(ID → Window) | 複数 Window を開くが、役割が固定 | 破棄時の後始末(辞書から除去)を忘れるとリーク要因 |
| 現在アクティブな Window を追跡 | デスクトップで “今操作しているウィンドウ” を確実に切り替えたい | プラットフォーム差異が出やすい。イベントで追跡する設計が必要 |
パターンC:ナビゲーションだけ初期化する(Shell / NavigationPage)
「画面をリセットしたい」と言いつつ、本当は “ナビゲーション履歴だけ消してトップへ戻したい” だけのことも多いです。この場合、ルートページの差し替えよりも軽量で、ユーザー体験も自然な方法があります。
| やりたいこと | おすすめ | 例 |
|---|---|---|
| Shell の特定ルートに戻る(履歴をリセット) | GoToAsync("//...") | await Shell.Current.GoToAsync("//home"); |
| NavigationPage のルートに戻る | PopToRootAsync() | await Navigation.PopToRootAsync(); |
| UI 構造自体を作り直したい(メニューも変えたい) | Window.Page の差し替え | window.Page = new AppShell(); |
「Shell のメニューやタブを、ユーザーの権限に合わせて動的に変えたい」など、UI 構造ごと変える必要があるなら Window.Page を差し替える価値があります。一方で、単にトップへ戻るだけなら GoToAsync の方が実装も軽く、状態も残せて便利です。
ログアウト後にトップへ戻す実装例:認証状態でルートを切り替える
実務では「ログアウトしたらログイン画面へ」「ログインしたら AppShell へ」という切り替えが王道です。CreateWindow() で起動時のルートを決め、ログアウト時は同じルールで戻すようにしておくと、挙動が一貫します。
起動時:認証状態でルートを分岐
public partial class App : Application
{
public static Window? WindowInstance { get; private set; }
private readonly IAuthState _authState;
public App(IAuthState authState)
{
InitializeComponent();
_authState = authState;
}
protected override Window CreateWindow(IActivationState? activationState)
{
Page root = _authState.IsSignedIn
? new AppShell()
: new NavigationPage(new LoginPage());
var window = new Window(root);
WindowInstance = window;
return window;
}
public static void ResetToSignedOut()
=> ResetTo(new NavigationPage(new LoginPage()));
public static void ResetToSignedIn()
=> ResetTo(new AppShell());
public static void ResetTo(Page newRoot)
{
MainThread.BeginInvokeOnMainThread(() =>
{
var window = WindowInstance ?? Application.Current?.Windows?.FirstOrDefault();
if (window == null)
return;
window.Page = newRoot;
});
}
}
この形にしておくと、ログイン・ログアウトの “ルート切り替え” が App に集約され、画面や ViewModel が複雑になりにくいです。
ログアウト処理:セッション破棄+画面リセットをセットで
public class SettingsViewModel
{
private readonly IAuthService _authService;
public SettingsViewModel(IAuthService authService)
{
_authService = authService;
}
public async Task LogoutAsync()
{
await _authService.SignOutAsync();
// UI をログイン画面へ戻し、戻る履歴を消す
App.ResetToSignedOut();
}
}
ログアウトは「トークンを消したのに画面が残っている」状態が最も危険です。サインアウト処理が成功したら、速やかにルートページを差し替えて UI を作り直すと、戻る操作やディープリンクで前画面が出る事故を抑えられます。
どの方法が最適か:リセット手段の比較表
最後に、現場で迷いがちな選択肢を “目的別” に整理します。
| 方法 | できること | 向いている目的 | 注意点 |
|---|---|---|---|
Window.Page = new ... | ルート UI を再生成(実質フルリセットに近い) | ログアウト、権限変更、セットアップ完了後のルート切り替え | DI/Singleton は残る。イベント購読やキャッシュは別途クリアが必要 |
Shell.Current.GoToAsync("//...") | Shell の履歴をリセットしてルートへ | ホームへ戻す、タブ移動の統一、深い階層からの復帰 | Shell 自体は作り直さない。メニュー構造を変えたい場合は不向き |
Navigation.PopToRootAsync() | NavigationPage の履歴を戻す | 単純な戻り履歴リセット | Shell と併用している場合は設計がブレやすい |
Application.Current.MainPage = ... | (単一 Window 前提なら)ルート差し替え | 古いコード資産の延命、シンプルなアプリ | .NET 9 の Window 起点・マルチウィンドウで意図が曖昧になりやすい |
よくある質問とトラブルシューティング
リセットしたのに「前の画面の情報」が残って見える
ルートページを差し替えても、次のような “アプリ内状態” が残っていると、結果的に同じ画面に見えてしまうことがあります。
- ViewModel が Singleton で登録されていて、プロパティが保持されている
- 静的フィールドにユーザー情報を保持している
- イベント購読で古い ViewModel が生き残っている
ログアウトや権限変更で確実に初期化したいデータは、「どこに状態がいるか」 を棚卸しして、リセット時に明示的に消すのが近道です。
マルチウィンドウ環境で、違うウィンドウが切り替わってしまう
静的に 1 個だけ Window を保持していると、意図せず別ウィンドウを差し替えることがあります。対策は次のいずれかです。
- リセット対象の Window を引数で渡す(呼び出し元が責任を持つ)
- Window を ID 管理して、どのルートをどの Window に割り当てるか決める
- アクティブ Window 追跡の仕組みを入れる
Shell を作り直したらルーティングや DI が絡んで例外が出る
Shell でルートを組み立て直すときは、ルートページ生成時に参照しているサービスやルート登録の順序が影響します。次のチェックが有効です。
- ルート生成に必要なサービス(ViewModel/Service)が DI に登録されているか
- ルート(Route)の登録が AppShell のコンストラクタなどで確実に行われているか
- 初期表示のルート(
//から始まるルート)へ遷移する前に Shell が表示済みか
まとめ:.NET 9 の画面リセットは「Window 起点」で考えるのが最短
.NET MAUI(.NET 9)では、起動の中心が CreateWindow() に寄り、Window がルートページを持つ構造がより前面に出ます。従来の MainPage 差し替えで悩んだら、次の方針に切り替えるのがスムーズです。
- 「画面を作り直したい」なら
Window.Pageを差し替える - 「履歴だけ戻したい」なら Shell のルート遷移や PopToRoot を使う
- リセットしても残る状態(DI/キャッシュ/イベント購読)を意識して設計する
ログアウトや権限変更といった “戻れたら困る” 画面遷移こそ、Window ルート差し替えが効きます。まずはシングルウィンドウ前提で実装し、将来マルチウィンドウにする可能性がある場合は、Window 選択の設計だけ先に決めておくと安心です。

コメント