Nexta Tech Blog
🔥

Hot Reloadの「適用成功」と「画面が変わる」は別物。WinForms/WPF/MAUI/Blazorで比較

に公開

はじめに

こんにちは!
ネクスタで、SmartFの開発エンジニアをしている日野岡です。

私は現在、WinFormsからBlazor Serverへの移行プロジェクトを進めていて、両方を日常的に触っています。

Blazorでは、.razorファイルを保存し、Visual StudioのHot Reload(🔥ボタン)を押すと、すぐにブラウザの表示が変わります。
この快適さに慣れたまま、WinFormsのフォームを編集した後、Blazorと同じ操作をしても、すぐに反映されず違和感を感じることがあります。

この違いが気になり、あらためて調べてみました。

この記事では、WinForms(.NETおよび.NET Framework 4.8)、WPF、.NET MAUI、Blazor Server、Blazor WebAssemblyを対象に、どの編集が適用でき、実行中の画面にどこまで反映されるのかを整理しました。
ポイントは、○と×の間にある「△」です。

比較する環境は以下の通りです。

環境
最新環境 Visual Studio 2026(18.x)+ .NET 10
現役環境 Visual Studio 2022 17.14 + .NET 8(LTS)

WinFormsについては、.NET Framework 4.8とVisual Studio 2022の組み合わせも加えました。
(この環境が現役なので、当然外せません・・・)

MAUI Blazor Hybridについて

今回は、MAUI Blazor Hybridは対象外としています。Blazor Hybridでは、RazorコンポーネントがWebAssemblyではなくMAUIアプリと同じ端末上の.NETランタイムで動作し、BlazorWebViewへ描画されます。C#、Razor、MAUI XAMLでHot Reloadの経路が異なり、対象プラットフォームによって使用するランタイムも変わるため、本稿の表だけでは正確に整理できないと判断しました。対象外であることは、Hot Reloadに対応していないという意味ではありません。

WinFormsで「△」になる理由

後述する「UIフレームワークごとの編集」の表で、WinFormsのInitializeComponent()内にある既存コントロールのプロパティを変更した時に、「△」になる理由は、なぜでしょうか。

WinFormsでフォームにボタンを置いたり、位置を変えたりすると、その内容は*.Designer.csInitializeComponent()に書き込まれます。
このメソッドが呼ばれるのは、フォームのコンストラクターが実行されるタイミングです。
つまり、通常はフォームを作るときの一度だけになります。

一方、Hot Reloadの内部で使われているEdit and Continue(EnC)は、変更後のILをランタイムへ送り、「次にそのメソッドが呼ばれたとき」から新しいコードを使えるようにします。
すでに実行し終えた処理を巻き戻し、もう一度呼び直してくれるわけではありません。

実行中のフォームでボタンの位置を変えた場合、次の順序になります。

  1. InitializeComponent()のコードが変更される
  2. Hot Reloadが差分をランタイムへ適用し、Visual Studioには成功と表示される
  3. 表示中のフォームは、変更前のInitializeComponent()ですでに構築されている
  4. 表示中のフォームは変わらないが、変更適用後にフォームの新しいインスタンスを生成すると、更新後のInitializeComponent()が実行され、新しいレイアウトになる

つまり、Hot Reload自体は効いているが、「ランタイムへ変更を適用すること」と「表示中のUIを作り直すこと」は別の処理であることが、「△」になる理由のようです。

Blazorではなぜ画面まで変わるのか

同じ理屈で考えると、Blazorでも描画済みのコンポーネントは古いまま残りそうです。

しかし実際には、ASP.NET CoreがHot Reloadの適用後にコンポーネントの再レンダリングを呼び出すため、上記の結果にはなりません。MVCやRazor Pagesでも、ブラウザーの更新が自動で走ります。

.NET MAUIのXAML Hot Reloadも似ており、変更されたコントロールのプロパティを実行中のUIへ適用し直すため、ページ全体を作り直さずに、フォーカス、入力値、ナビゲーション位置などを保てます。

つまり、BlazorやMAUIには、Hot Reload後にUIを再レンダリングまたは再適用するフレームワーク側の仕組みがあるが、(レガシーな仕組み故に、、)WinFormsには、InitializeComponent()の変更を表示中のフォームへ自動適用する機能が無いことが、原因となっているようです。

WinFormsとBlazor/MAUIのHot Reload対比表

WinFormsとBlazor/MAUIのHot Reload対比表

Hot Reload対応状況表(UIフレームワーク別)

表中の記号の説明

記号 意味
編集が適用され、実行中のUIにも反映される
編集はランタイムに適用されるが、表示済みのUIには自動反映されない。反映には、対象の初期化処理を再実行するか、新しいUIインスタンスを生成する必要がある(※「WinFormsで「△」になる理由」の事象)
× Rude Edit。変更を適用できず、再起動が必要
そのフレームワークには該当する概念がない
? 未検証。情報募集中

UIフレームワークごとの編集

編集パターン WinForms WPF MAUI Blazor Server Blazor WASM
InitializeComponent()内の既存コントロールのプロパティ変更
XAMLの編集(プロパティ変更など)
XAMLへのx:Name付き要素の追加 ○? ○ ※1
.razorマークアップの編集
@codeブロック内の既存メソッド本体の変更
コンポーネントのパラメーター定義を追加 ○? ○?
呼び出し側でコンポーネントのパラメーター属性を削除 ? ※2 ? ※2
scoped CSS(.razor.css)の変更
ファイルやNuGetパッケージの追加または削除 × × × × ×
Razorファイルの名前変更 ○ ※3 ○ ※3

まとめ

今回は、WinFormsで感じていた違和感をきっかけに、Hot Reloadが成功しているのに、表示中のフォームが変わらない理由を調査してみました。

調査して分かったのは、ランタイムへ変更を適用できることと、その変更が表示中のUIへ反映されることは別、ということです。
BlazorやMAUIは、Hot Reload後にUIを再レンダリングまたは再適用するフレームワーク側の仕組みを持っています。一方、WinFormsには、InitializeComponent()の変更を表示中のフォームへ自動適用する標準機構はありません。
だから、Hot Reloadは成功しているのに画面が変わらない、という△が生まれます。

WinFormsからBlazorへの移行を進めていると、この△と○の差は、そのまま日々の開発体験の差として現れてきます。
保存した直後に画面が変わるのは、やはり便利です。
ただ、WinFormsの挙動も仕組みが分かれば、「Hot Reloadが壊れている」と迷わずに済みそうです。

なお、Hot Reloadで扱える編集は、ランタイムとコンパイラーの組み合わせによっても異なります。
以降の付録では、その違いとHot Reloadの歴史について、今回の調査で副次的に整理した内容をまとめます。

付録1:Hot Reload対応状況表(ランタイム/.NET別)

以降の付録は、今回の調査で副次的に整理したものです。
個人的な整理の意味合いが強いので、興味のある方だけ、お読みください。

C#編集の代表的な対応状況(公式資料を基にした目安)

CoreCLRおよびMono列は、Visual Studio 2026(18.x)+ .NET 10を基準としています。
.NET Framework 4.8列のみ、Visual Studio 2022 17.14との組み合わせです。

C#コードをどこまで変更できるかは、UIフレームワークよりも、動作しているランタイムの影響を強く受けます。そのため、この表はランタイム別に分けています。

編集パターン CoreCLR
(WinForms、WPF、Blazor Server)
.NET Fx 4.8
(WinForms)
Mono
(Blazor WASM、MAUI Android/iOSの既定Debug構成)
メソッド本体の変更
既存型への新規メソッド追加 ○ ※7
既存型への新規フィールド追加 ○ ※4 ○ ※4 ※7
新規プロパティやイベントの追加 ○ ※7
新規型(クラスなど)の追加
ラムダ本体の変更(キャプチャ不変) ○?
ラムダの追加 ○ ※5 ○ ※5 ○ ※5
ラムダのキャプチャ集合の変更 × × ×
ジェネリックメソッドまたは型の編集 × ○(.NET 8以降)
asyncまたはイテレータ本体の編集 ○?
新規await式またはyield式の追加 ○ ※6 ×? ×
フィールド以外のメンバーの改名 ×? ×?
パラメーター型や戻り値型の変更 ×? ?
属性の追加または変更 ×? ○?
インターフェイスの変更 × × ×
abstract、virtual、overrideメンバーの追加 × × ×
enumへのメンバー追加 × × ×

CoreCLRでは、フィールド以外のメンバー名や、パラメーター型、戻り値型の変更も適用できます。

Visual Studio 2022 17.4以降では、フィールド以外のメンバー名や、メソッドの引数・戻り値の型もHot Reloadで変更できます。ただし、すべての定義変更に対応しているわけではありません。以前と同じように「メソッドの定義を変えたら必ず再起動が必要」と考えていると、現在の対応範囲を正しく判断できないため注意が必要です。

その一方で、enumへのメンバー追加は現在も「×」で、地味につらい制約が続いています。

なお、DI登録、Program.cs、ミドルウェア、ルート作成の変更は、この表から外しました。
DI登録やミドルウェア、ルートは、アプリの起動時に組み立てられます。
Hot ReloadでProgram.csを変更しても、起動時の処理が最初から実行されるわけではありません。そのため、DIの登録先を変えたり、新しいミドルウェアやルートを追加したりした場合は、原則として再起動が必要です。

一方、すでに登録されているミドルウェアやルートハンドラーの処理内容は、Hot Reloadで反映される場合があります。「処理の中身を変更したのか」「起動時に作られる構成を変更したのか」を分けて考えることが大切です。

同じC#でも結果が変わる理由

「C#編集の代表的な対応状況」の表を見ると、CoreCLRでは適用できても、.NET Framework 4.8やBlazor WebAssemblyが使用するMonoでは、×または×?となる編集があります。

例えば、.NET Framework 4.8ではジェネリックコードの編集、Monoでは新しいawait式やyield式の追加が該当します。なぜ、同じC#の編集でもランタイムによって結果が変わるのでしょうか。

Microsoft Learnには、Hot Reloadで可能な編集の種類は、ランタイムとコンパイラーのバージョンで決まると書かれています。
F5(デバッガーあり)で起動したか、Ctrl+F5(デバッガーなし)で起動したかだけで決まるわけではありません。

内部では、ランタイムがEnCで扱える変更を、capability(能力)の一覧としてRoslynへ伝えます。
例えば、BaselineAddMethodToExistingTypeNewTypeDefinitionGenericUpdateMethodなどです。
Roslynはその一覧を見て、ランタイムが扱えない編集をRude Editと判定します。

dotnet watchの詳細ログを見ると、Hot reload capabilities: Baseline AddMethodToExistingType ...というメッセージとともに、能力の一覧が出力されます(.NET 6〜7時代のdotnet watchではApplication supports the following capabilities...という文言でした)。
この仕組みを知ると、先ほどの表が少し読みやすくなります。

Hot Reloadで扱える編集が増えてきた背景には、ランタイム側のEnC能力の拡張があります。
ただし、編集の可否はランタイムだけで決まるものではありません。
ジェネリック編集のようにランタイム側の対応が必要な変更もあれば、コンパイラーやVisual Studioの更新によって扱えるようになった変更もあります。
環境を比較するときは、Visual Studio、コンパイラー、ランタイムを一組として見る必要があります。

Visual Studio 2022 17.14と.NET 8の場合

最新環境から、現在もよく使われているVisual Studio 2022 17.14と.NET 8へ戻しても、違いは意外と多くありません。

項目 Visual Studio 2026 + .NET 10 Visual Studio 2022 17.14 + .NET 8
Mono(WASM、MAUI)のジェネリック編集
Razor Hot Reloadの速度と安定性 RazorコンパイラーをRoslynプロセスにcohostして高速化 従来版。遅さや不安定さを指摘する報告が多かった
Rude Editを踏んだとき 対応する一部のプロジェクトでは<HotReloadAutoRestart>による再起動が可能 Rude Editダイアログからの再ビルドや、dotnet watchによる再起動が可能
Razorファイルの名前変更 ○(18.3以降) ×(再ビルドが必要)
C#の編集可否(CoreCLR) 対応状況表のとおり ジェネリック編集には対応済み。編集ごとの差分は実機確認が必要

ジェネリック編集は.NET 8の時点で、CoreCLRだけでなくWebAssemblyでも対応しています。
今回確認した公式情報では、Visual Studio 2022 17.14とVisual Studio 2026の目立つ違いは、Razorの処理方式やファイル名変更への対応、再起動の支援など、開発ツール側に多く見られました。
ただし、CoreCLRの対応範囲を編集項目ごとに比較したわけではないため、「ほぼ同じ」とは断定しません。

対応できない編集を減らすだけでなく、Rude Editが発生した後の再起動を支援する。
Hot Reloadの改善点も、少しずつ変わってきたように感じます。

なお、<HotReloadAutoRestart>はすべてのプロジェクトに共通する機能ではありません。
Visual Studio 2026のリリースノートでは、プロセスをすばやく再起動できる一部のAspireやWebプロジェクトなどが対象として説明されています。

付録2:Edit and ContinueからHot Reloadまでの歴史

ここまでの変化を、時系列でも確認してみます。

  • Visual Studio 2019 16.10では、partial class、ソースジェネレーターが生成したファイル、usingの追加を編集できるようになりました
  • Visual Studio 2019 16.11では、デバッガーで実行中のコードへ変更を反映する最初のHot Reload体験が入り、ツールバーに「Apply Code Changes」が追加されました
  • Visual Studio 2022 17.0と.NET 6では、🔥ボタンを使う現在のHot Reload体験が本格化しました。.NET 6以降ではデバッガーなしのCtrl+F5にも対応し、Razor、CSS、属性の変更も扱えるようになりました。dotnet watchからHot Reloadを削除する方針が発表され、反発を受けて撤回されたのもこの年です
  • Visual Studio 2022 17.3から17.4では、メンバーの削除や改名、パラメーター型、戻り値型の変更に対応しました
  • Visual Studio 2022 17.7と.NET 8では、CoreCLRのランタイム変更によってジェネリック編集が使えるようになりました。.NET 8のWebAssemblyランタイムでも、CoreCLRと同等のHot Reload対応とジェネリック型の編集が案内されています
  • Visual Studio 2022 17.14では、WPFのXAML Hot Reloadをデバッグしていないときにも利用できるようになりました
  • Visual Studio 2026 18.3では、Razorコンパイラーのcohost化による高速化、Razorファイル名変更への対応、一部プロジェクトでのHotReloadAutoRestartが入りました

出典

Nexta Tech Blog
Nexta Tech Blog

Discussion