OnInitialize で NavigationManager.NavigateTo すると例外発生する場合がある
ずばり、表題のとおりです。
例えば以下のように実装されたページコンポーネントがある、標準的なプロジェクトテンプレートから生成された Blazor Server アプリケーションがあるとします。
@page "/articles"
@inject NavigationManager NavigationManager
@code {
protected override void OnInitialized()
{
this.NavigationManager.NavigateTo("./articles/1");
}
}
上記実装は、"https://.../articels" の URL に遷移 (ナビゲーション) されたら、さらに "https://.../articels/1" の URL に遷移 (いわゆる "リダイレクト" 的な動作) することを想定しています。実際、いったん、"https://.../" などの URL にアクセスして、Blazor Server としての対話モードが機能し始めてから、ページ内のリンク経由などで "https://.../articels" に遷移 (ナビゲーション) した場合は、期待したように動作します。
しかし、この Blazor Server アプリケーションに対して、"https://.../articels" の URL をブラウザのアドレスバー直接入力してアクセスすると、NavigationException
例外が発生する場合があります。
ただし、この NavigationException
例外、とくにログ出力されるわけではないようで、かつ、その後のレンダリング処理もそれなりに行なわれるようであるため、このような実装をしていても気づかずに済んでいるかもしれません。Windows 上で Visual Studio を使ってのデバッグ実行時などに、例外発生が報告されて気づくことがあるような感じです。
なぜ例外が発生してしまうのか
このような例外が発生してしまうのは、サーバー側レンダリングがその原因の一端です。Blazor の NavigationManager における NavigateTo 動作は、Web ブラウザの History API などを呼び出して、遷移先の URL をブラウザのアドレスバーに示すなどの処理を行ないます。しかしサーバー側レンダリングの最中は、まだ、ブラウザ内の資源にアクセスできる状態にありません。つまり、Web ブラウザから、指定の URL のドキュメントをください、という HTTP GET 要求が届いたばかりで、その返信用の HTML 文字列を組み立てている最中、というのがサーバー側レンダリングで行なわれていることだからです。
そのような状況なのに、ブラウザの API を触ろうとする処理 = NavigationManager の NavigateTo メソッド呼び出しなどを行なってしまったことで、前述の例外に至ってしまう、という流れになります。
このような仕組みであるため、Blazor Server や Blazor SSR に限らず、Blazor WebAssembly であっても、プリレンダリングを行なっている場合は、同じ問題が発生します。
改善方法
プリレンダリングをやめる
このような例外を発生させない回避策のとしては、プリレンダリングを行なわない、という方法があります。
例えばクラシカルな Blazor Server アプリケーションの場合、フォールバック Razor Pages 内に、アプリケーションコンポーネントをレンダリングするための下記のようなタグヘルパーが記述されていると思いますが、
<component type="typeof(App)" render-mode="ServerPrerendered" />
このタグヘルパー中の render-mode
指定を、ServerPrerendered
から Server
に変更することで、サーバー側プリレンダリングが発生しなくなり、必ずブラウザとの対話モードが機能しはじめてから OnInitialized
などのライフサイクルメソッドが実行されるようになりますので、NavigationManaget.NavigateTo
も例外を起すことがなくなります (下記実装例)。
<component type="typeof(App)" render-mode="Server" />
ただし、当然のことながら、プリレンダリングをやめる以上、プリレンダリングの利点 (ページが表示されるまでの速度向上や、SEO 対策) はすべて失われます。
OnAfterRender
で実行する
もうひとつの方法は、OnInitialized
ライフサイクルメソッドではなく、OnAfterRender
ライフサイクルメソッド内で NavigationManager.NavigateTo
メソッド呼び出しを行なう、という方法があります。OnAfterRender
ライフサイクルメソッドのタイミングであれば、ブラウザ上での対話モードが機能している状況であることが保証されていますので、このタイミングであれば NavigationManager.NavigateTo
メソッドは正しく動作します (下記実装例)。
@page "/articles"
@inject NavigationManager NavigationManager
@code {
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
this.NavigationManager.NavigateTo("./articles/1");
}
}
}
RendererInfo
を参照する
あるいはまた、.NET 9 から利用可能になった、Razor コンポーネントのプロパティ、RenderInfo
を参照し、対話モードが機能しているかどうかを if 文で判定して、OnInitialized
ライフサイクルメソッド中で NavigationManager.NavigateTo
を呼び出す方法も考えられます (下記実装例)。
@page "/articles"
@inject NavigationManager NavigationManager
@code {
protected override void OnInitialized()
{
// 対話モードが機能している最中か?
if (this.RendererInfo.IsInteractive)
{
// もしそうなら、NavigateTo しても大丈夫
this.NavigationManager.NavigateTo("./articles/1");
}
}
}
まとめ
サーバー側レンダリング (プリレンダリング) の最中は、NavigationManager.NavigateTo
は使えず、呼び出すと NavigationException
例外が発生します。NavigationManager.NavigateTo
は、対話モードが機能しているときだけ使えるので、その点に配慮して呼び出しを工夫するのが最善かと思われます。
もっとも、何も考慮せずに、雑に OnInitialized
ライフサイクルメソッド内で NavigationManager.NavigateTo
を呼び出していても、それでサーバー側プリレンダリング中に例外が発生していても、実質的な害はなさそうに見えるので、それはそのままでもいいのかもしれません。ただ、もしかすると、Application Insights などの APM のログに例外が記録されるかもしれないため (未確認、コンソール出力に例外メッセージが表示されていないところを見ると記録されないかも)、その場合は、無用な例外メッセージで汚染されると思いますので、本記事で紹介したような何らかの処置はすべきではないか、と思われました。
その他、以前に投稿した以下の記事もあわせて参照されるとより理解が深まるかも知れません、ご参考までに。