[XAML] Incremental XAML Hot Reload (source-generated patch chains) - #34338
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34338Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34338" |
There was a problem hiding this comment.
Pull request overview
This PR adds incremental XAML Hot Reload support to the XAML Source Generator pipeline by diffing XAML trees across generator runs and emitting versioned UpdateComponent_vNtoM() methods plus a per-page [MetadataUpdateHandler] dispatcher. Runtime support is introduced via a component registry to map live instances to stable node IDs.
Changes:
- Introduces a XAML tree diff engine + stable node-id assignment and uses them to generate
UpdateComponent_vNtoM()source on property-only edits. - Adds runtime
XamlComponentRegistry(with PublicAPI entries) used by generated code and the metadata update handler. - Adds/updates unit and integration tests and extends the source-gen test driver to plumb the MSBuild opt-in flag.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Controls/src/SourceGen/XamlGenerator.cs | Wires incremental HR into generator output; emits UC + handler sources; updates in-proc state cache. |
| src/Controls/src/SourceGen/InitializeComponentCodeWriter.cs | Emits __version field + registry registration; adds helpers for root type resolution and UC generation. |
| src/Controls/src/SourceGen/UpdateComponentCodeWriter.cs | New UC code generator producing UpdateComponent_vNtoM() methods from diffs. |
| src/Controls/src/SourceGen/MetadataUpdateHandlerCodeWriter.cs | New generator emitting [MetadataUpdateHandler] glue to apply updates to live instances. |
| src/Controls/src/SourceGen/XamlNodeDiff.cs | New property-only diff engine for parsed XAML trees (structural changes => null). |
| src/Controls/src/SourceGen/NodeIdHelper.cs | New helper assigning stable {Type}_{depth}_{index} node IDs. |
| src/Controls/src/SourceGen/XamlHotReloadState.cs | New in-process cache for previous XAML + version per (assembly, relative path). |
| src/Controls/src/Xaml/XamlComponentRegistry.cs | New runtime registry (weakly keyed by instance) for nodeId → component and instance enumeration. |
| src/Controls/src/Xaml/PublicAPI/*/PublicAPI.Unshipped.txt | Adds new public API entries for XamlComponentRegistry across TFMs. |
| src/Controls/tests/SourceGen.UnitTests/SourceGeneratorDriver.cs | Extends test driver options to include EnableMauiIncrementalHotReload. |
| src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SourceGenXamlInitializeComponentTests.cs | Updates hint-name filtering and adds IHR opt-in parameter plumbing. |
| src/Controls/tests/SourceGen.UnitTests/InitializeComponent/IncrementalHotReloadICTests.cs | New tests validating IC emits __version + registry registrations when IHR is enabled. |
| src/Controls/tests/SourceGen.UnitTests/XamlNodeDiffTests.cs | New unit tests for diff behavior (property diffs vs structural null). |
| src/Controls/tests/SourceGen.UnitTests/NodeIdHelperTests.cs | New tests verifying stable node ID assignment. |
| src/Controls/tests/SourceGen.UnitTests/UpdateComponentCodeWriterTests.cs | New tests for UC generation output shape and content. |
| src/Controls/tests/SourceGen.UnitTests/XamlIncrementalHotReloadPipelineTests.cs | New “two-run” integration tests covering generator replay behavior and UC/handler emission. |
| src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj | Enables MetadataUpdaterSupport for tests involving hot reload infra. |
| src/Controls/tests/Core.UnitTests/XamlComponentRegistryTests.cs | New unit tests validating registry behavior (register/tryget/unregister/getinstances). |
| // Emit Register calls and __version bump for incremental hot reload | ||
| if (nodeIds != null) | ||
| { | ||
| codeWriter.WriteLine(); | ||
| foreach (var kvp in sgcontext.Variables) | ||
| { | ||
| if (kvp.Key is ElementNode en | ||
| && nodeIds.TryGetValue(en, out var nodeId) | ||
| && !string.IsNullOrEmpty(nodeId)) | ||
| { | ||
| codeWriter.WriteLine($"global::Microsoft.Maui.Controls.Xaml.XamlComponentRegistry.Register(this, \"{nodeId}\", {kvp.Value.ValueAccessor});"); | ||
| } | ||
| } | ||
| codeWriter.WriteLine("__version = 1;"); | ||
| } |
There was a problem hiding this comment.
When EnableDiagnostics hot-reload fallback triggers (InitializeComponentRuntime(); return;), the new incremental-hot-reload bookkeeping (XamlComponentRegistry.Register(...) + __version = 1;) is skipped entirely. That leaves instances unregistered and/or with stale __version, so subsequent incremental updates will consistently fail/fallback. Consider explicitly opting the instance out before returning (e.g., unregister + reset __version), or ensure the runtime-inflation path also sets up registry/version so IHR can resume after a runtime reload.
There was a problem hiding this comment.
Good observation. The legacy ResourceProvider2 fallback (Xamarin-era pure-XAML hot reload) is a coexistence path: when it returns content, InitializeComponentRuntime re-parses the XAML and creates fresh children that the static IC code knows nothing about. We emit Unregister(this) + __version = 0 so the page's stale XIHR registrations are dropped, but we deliberately don't re-Register the new children (the legacy path doesn't know our node IDs). This makes the page a silent no-op for XIHR until the next normal Initialize — which is the right outcome: incremental hot reload is opt-in and meant to replace, not augment, the legacy ResourceProvider2 path. Happy to add an explicit code comment documenting this if you think it's worth it.
8f1a3cc to
459e08c
Compare
1e2557d to
9c34d31
Compare
|
/review -b feature/refactor-copilot-yml |
AI code review for net11.0 targetVerdict: Needs discussion (draft/WIP; non-approval automated review, no human approval implied) Large feature PR (XAML Incremental Hot Reload via the source generator) by the XAML/SourceGen area owner. ~45 files including a new diff/codegen pipeline, runtime Observations:
CI: required pipelines red, expected for a WIP draft; not assessed as merge-ready. Confidence: medium and intentionally high-level — main pre-merge items are the registry lifetime/leak question and final API review. |
09607a6 to
f276fc7
Compare
AI code review refresh for net11.0 targetHead reviewed: Prior-review reconciliation (round-15 → now)The new commit
CI status (build 1457900)Mixed. Classification:
Blast radius
Findings summary
Confidence: medium-high on the test-failure diagnosis (read from actual Helix results) and on the registry resolution (read at head); medium on the AOT/trim reds (not deep-dived). Automated non-approval review. No human approval is implied or given; this does not gate merge. |
2d9c193 to
7f0c8ee
Compare
AI code review refresh for net11.0 targetHead reviewed:
Prior‑review reconciliation (round‑16 → now)
New since round‑16 (this commit) — spot review
CI status
Blast radius
Findings summary
Confidence: high on the Automated non‑approval review. No human approval is implied or given; this comment does not gate merge and uses neither approve nor request‑changes. |
7f0c8ee to
b260645
Compare
AI code review refresh for net11.0 targetHead reviewed: Verdict: Needs changes — the new commit ( Prior-review reconciliation (round 17 → now)
New since round 17 (spot review of
|
PR #34338 — [XAML] Incremental XAML Hot Reload (source-gen)Verdict: NEEDS_DISCUSSION (confidence: medium). This is a large, sophisticated dev-time feature (a generated Worth confirming (hot-reload flow — dev-time behavior, not production)
|
This comment has been minimized.
This comment has been minimized.
- Gate EnableMauiIncrementalHotReload default-on to Debug only; explicitly resolve to true/false so Release/AOT keeps the runtime switch off (preserves FeatureGuard trimming) and avoids per-page registry calls in production apps - Remove MAUI1002 'experimental' warning now that SourceGen XAML HR ships in net11 - Revert source-gen EnableIncrementalHotReload fallback to false (targets now always pass an explicit value); fixes 98 SourceGen unit tests that opted out via null - Fix test drivers to encode opt-out as "false" instead of null - HotReloadDiagnostics: isolate event subscribers (a throwing listener no longer aborts the hot-reload batch); use Volatile.Read for CurrentVersion - MUH: allocate version range atomically (toVersion/fromVersion) only for non-empty batches; start Stopwatch at request time so Duration includes dispatch latency Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- UpdateComponent now wraps each expanded markup extension in its own block scope, so multiple markup-extension property changes on one element no longer re-declare the same locals (staticResourceExtension/xamlServiceProvider/...) → CS0128. Surfaced by enabling XIHR by default: a default 'dotnet new maui' app failed to build. - XamlNodeDiff.ToDebugString() uses '\n' instead of AppendLine() so output is deterministic across platforms (was failing XamlNodeDiffTests on Windows with \r\n). - Add regression test compiling generated UC for two markup-extension changes on one element. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
XAML Incremental Hot Reload E2E tests apply real metadata deltas via MetadataUpdater.ApplyUpdate, which on CoreCLR requires modifiable assemblies to be enabled at process launch (DOTNET_MODIFIABLE_ASSEMBLIES=debug). Helix doesn't set it, so MetadataUpdater.IsSupported was false and the E2E tests failed. Export it before each work item using shell-appropriate syntax for the target queue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The UpdateComponent incremental generator emitted 'parent.<ContentProperty> = (IView)__na_N', which fails to compile with CS0266 because a single content property (e.g. ContentPage.Content) is typed Microsoft.Maui.Controls.View, not IView, and IView -> View is not an implicit conversion. This surfaced to users as a message-less ENC1002 during hot reload. Assign the concrete child node directly instead of casting up to IView: the generated local is 'var __na_N = new <ConcreteType>()', which already implicitly converts to the content property type. Fixes both the root-content and nested content-container emission sites. The (IView) cast remains correct for layout children (IList<IView>). Fixes #36256. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The keep-both conflict resolution during the rebase concatenated base and branch entries, producing exact-duplicate lines (and a doubled #nullable enable header) in the Core/Xaml net + netstandard PublicAPI.Unshipped.txt files, which fail RS0025 (symbol appears more than once). Collapse duplicates; PublicAPI entries are a set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…4 leak) The incremental hot reload patch chain is stored in the process-global static XamlHotReloadState, hosted in a long-lived VBCSCompiler shared across many project builds. Keyed on (AssemblyName, TargetFramework, RelativePath), a patch chain could leak between two projects sharing an assembly name and a file name (e.g. two apps both named 'MauiApp.1' with a 'MainPage.xaml'). A leaked patch can reference a type the other project doesn't reference — e.g. a BlazorWebView (Microsoft.AspNetCore.Components. WebView.Maui) from a Blazor app bleeding into a plain MAUI app — producing generated UpdateComponent code that fails to compile with CS0234. This surfaced once XIHR was enabled by default: the plain 'dotnet new maui' template failed the Build integration tests (ResizetizerTests.CollectsAssets, SimpleTemplateTest.BuildsWithSpecialCharacters). Key the state on the XAML file's absolute path (ProjectItem.HotReloadStateKey), which is unique per project on disk yet stable across incremental builds. Both the writer (XamlGenerator) and the reader (InitializeComponentCodeWriter) use the shared key. Adds a regression test proving a Blazor patch from one project can't leak into a plain project that shares its assembly name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…explicit - Rename the synthetic text-content diff property name from '_Content' to '__MAUI_Content__' so it cannot collide with a real property/x:Name (jonathanpeppers review). - Change the bare catch blocks around generator-time type conversion / markup parsing to explicit 'catch (Exception)' with comments explaining they are intentional catch-alls: a throwing converter or parser at generator time must degrade to the runtime fallback rather than crash the build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… content transitions - AddedNamedElement_IsTrackedViaRegistry: a hot-reload-added <Button x:Name='Foo'/> is registered by node id (Register(this, ...)) and does NOT assign the non-existent this.Foo backing field (EnC can't add fields to a loaded type). - ContentTransitions_StringToMarkupToElementAndBack_Compile: a value transitioning string -> markup extension -> string produces compilable UC at each step. Both requested by jonathanpeppers in review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
XIHR defaults to off everywhere (opt-in via <EnableMauiIncrementalHotReload>true</>) instead of on-by-default in Debug. This matches the Preview 6 plan and resolves the 3 test failures (TestSourceGenInflator, HotReloadWorks(SourceGen), HotReloadSupportForXSG) that were caused by the legacy Hot Reload fallback being disabled when XIHR was on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…v var The 5 XamlIncrementalHotReloadE2ETests apply real metadata deltas via MetadataUpdater.ApplyUpdate, which on CoreCLR requires MetadataUpdater.IsSupported to be true. That is only the case when the process is launched with DOTNET_MODIFIABLE_ASSEMBLIES=debug (verified: the runtimeconfig switch alone is not enough on CoreCLR). Helix wasn't providing it, so these tests failed the unit-test leg. - Add MetadataUpdateFactAttribute: skips (rather than fails) when MetadataUpdater is unsupported, mirroring how dotnet/runtime gates its own ApplyUpdate tests. The tests still run wherever the env var is set (local dev, VS, or Helix once the env var lands). - Fix eng/helix.proj: the single send targets both a Windows (cmd) and an osx (bash) queue, so the per-shell $(IsPosixShell) condition could only ever be correct for one. Emit both 'export' and 'set' forms unconditionally so the variable is exported on whichever shell each work item uses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ooling (#36459) ### Description Incremental contribution to `feature/xaml-incremental-hotreload`. Extends `HotReloadDiagnostics` so IDE tooling (the Visual Studio / VS Code MAUI Hot Reload diagnostics, a.k.a. "XamlTools") can **classify** a hot reload cycle as XAML Incremental Hot Reload (XIHR), **report** its apply stats, and **surface** per-instance failures. Today the tooling only sees a generic managed-code delta, so it misclassifies XAML SourceGen edits as plain C# and can't report the incremental apply result. All changes are **purely additive** to the diagnostics surface — no change to the hot reload apply itself, and no breaking API changes. #### New / changed public API (`Microsoft.Maui.Controls.Xaml.Diagnostics`) - **`HotReloadDiagnostics.UpdateSkipped`** — new event. Terminal signal raised when an update is *recognized* (has generated `UpdateComponent()` types) but nothing is dispatched because there are no live instances to patch, so observers always get a definite outcome even when `UpdateApplied` never fires. - **`HotReloadRequestedEventArgs.HandledTypes`** — the recognized incremental-XAML subset of `UpdatedTypes`. Raised **synchronously, before any UI-thread dispatch**, so tooling can classify the update type inline (XIHR vs. non-XAML) without awaiting the async apply. - **`HotReloadErrorEventArgs.Version`** — the update-cycle version a per-instance failure belongs to (matches the corresponding `HotReloadAppliedEventArgs.ToVersion`), so failures can be correlated with their apply. - **`HotReloadSkippedEventArgs`** — new event args for `UpdateSkipped` (`UpdatedTypes`, `HandledTypes`, `Timestamp`). #### Handler (`XamlIncrementalHotReloadHandler`) - Builds the `handledTypes` set and raises `OnUpdateRequested` synchronously before dispatch. - Raises `OnUpdateSkipped` for a recognized-but-empty batch (returns without dispatch). - Passes `toVersion` to `OnUpdateFailed`. - Allocates the diagnostic version only for **non-empty** batches, keeping the version stream gap-free (every increment is paired with an `UpdateApplied`). #### Firing contract (documented in-code) The three files carry brief "XamlTools contract" comments describing the reflection-by- name binding surface tooling relies on: the type/event/property names, the `EventHandler<T>` shapes, and the ordering guarantees — `UpdateRequested` is synchronous and pre-dispatch; a dispatched batch ends with exactly one terminal `UpdateApplied` (always raised, even if every instance failed) or `UpdateSkipped`; each `UpdateFailed` precedes its batch's `UpdateApplied`. ### Issues Fixed Fixes # <!-- link the XIHR-diagnostics tracking issue, if any --> ### Testing - Added `XamlIncrementalHotReloadHandlerTests` (7 tests, `Controls/tests/Core.UnitTests`) covering: `UpdateRequested`/`HandledTypes` classification, `UpdateApplied` instance/version/duration, `UpdateFailed` per-instance + `Version` correlation, and the `UpdateSkipped` no-live-instances path. Uses a `MainThread` custom-implementation harness for the UI-thread dispatch. - Verified end-to-end on a physical Android device with the consuming IDE tooling: a XAML SourceGen edit is classified as `xaml-sourcegen` and the IDE reports the incremental apply (instance count, `version 0→1`, duration). ### API Changes Additive only — see the updated `PublicAPI.Unshipped.txt` for all TFMs.
…36482) Under source-generated XAML Incremental Hot Reload, editing a control inside a DataTemplate crashed the app / poisoned Hot Reload / killed dotnet watch. Root cause: a DataTemplate's content is emitted as an anonymous lambda (dataTemplate.LoadTemplate = () => { ... }) inside InitializeComponent. The generator regenerates InitializeComponent on every edit, and anonymous lambdas have unstable synthesized-closure identity across regenerations, producing invalid Edit-and-Continue deltas (deleted/renamed synthesized closure methods -> the 'Bad binary signature (0x80131192)' / EnC NullReferenceException / silent app exit symptoms). Controls inside a DataTemplate are created per-cell and are not registered in XamlComponentRegistry, so the incremental UpdateComponent patch is empty for them and the edit relies entirely on the (fragile) EnC re-apply of InitializeComponent. Edits outside a DataTemplate are plain statements (no lambda) and hot-reload fine. Fix: under Incremental Hot Reload, emit the template body as a stably-named local method (object LoadTemplate_{line}_{pos}()) referenced by name instead of an anonymous lambda. The name is derived from the template content root's source position, so it stays constant across successive property-value edits, giving EnC a stable name anchor while preserving capture semantics. Non-HR builds keep the anonymous lambda (no change to production/AOT/trimming output). Adds SourceGen unit tests asserting the named-method shape, name stability across an edit, and that the generated code (including a compiled binding inside the template) compiles. Note: unit tests validate the code-shape / EnC-stability invariant; final confirmation of the runtime fix requires on-device hot-reload testing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers review feedback (#34338): - XamlGenerator: replace empty 'catch { }' in the first-run HR seed with 'catch (Exception)' + a comment explaining the swallow is intentional (IC generation re-parses and the outer catch surfaces the real diagnostic). - CSharpExpressionHelpers: remove the IsKnownMarkupExtension back-compat shim and point its single caller at IsKnownMarkupExtensionName. - Remove '#region-in-disguise' banner/separator comments across the XIHR source and test files, and add a repo instruction (.github/copilot-instructions.md) to stop generating them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
d434913 to
1f3232b
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 53 out of 53 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
eng/helix.proj:31
- The current approach runs both
export ...andset ...as separateHelixPreCommands and assumes the non-native command is a harmless no-op. In practice,set DOTNET_MODIFIABLE_ASSEMBLIES=debugin bash is not a no-op (it changes positional parameters), and depending on how Helix executes pre-commands, the failingexporton Windows could also be treated as an error.
A safer pattern is to use a single command that works in both cmd and bash via ||: in bash export succeeds and short-circuits; in cmd export fails and set runs.
This single Helix send targets both a Windows (cmd) and an osx (bash) queue, so a per-shell
condition on $(IsPosixShell) can only ever be right for one of them. Emit both forms
unconditionally instead — each is a harmless no-op in the other shell (an unrecognized command
on the non-native shell), and the correct one exports the variable for that queue's work item.
-->
src/Controls/src/SourceGen/XamlHotReloadState.cs:22
- The doc comment says the cache is keyed by
(AssemblyName, TargetFramework, RelativePath)to prevent cross-project bleeding, but the implementation is now intentionally passingProjectItem.HotReloadStateKey(typically an absolute path) as the key component. Updating the wording here would avoid future readers assuming project-relative paths are sufficient for isolation.
/// maps a <c>(AssemblyName, TargetFramework, RelativePath)</c> tuple to the XAML content,
/// version counter, and the list of accumulated patch bodies (each an <c>if (__version == N)</c> block).
///
/// Keyed on <c>(AssemblyName, TargetFramework, RelativePath)</c> to prevent:
/// <list type="bullet">
| // Stable, unique name for a DataTemplate's generated LoadTemplate method. Derived from the | ||
| // template content root's source position so it stays constant across successive property-value | ||
| // edits (keeping the Edit-and-Continue identity stable); distinct templates have distinct | ||
| // positions. See dotnet/maui#36482. | ||
| static string TemplateLoadMethodName(INode node) | ||
| { | ||
| var line = node is IXmlLineInfo li && li.HasLineInfo() ? li.LineNumber : 0; | ||
| var pos = node is IXmlLineInfo li2 && li2.HasLineInfo() ? li2.LinePosition : 0; | ||
| return $"LoadTemplate_{line}_{pos}"; | ||
| } |
| if (Context.ProjectItem.EnableIncrementalHotReload) | ||
| { | ||
| var methodName = TemplateLoadMethodName(node); | ||
|
|
…tes (#36683) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description Hardens the XAML Incremental Hot Reload (XIHR, #34338) `DataTemplate` source-gen so it produces valid code across real-world XAML. This is a **prerequisite for enabling XIHR by default** (#36682) — with XIHR on, several repo projects (`Essentials.AI.Sample`, `Controls.TestCases.HostApp`, `Controls.Xaml.UnitTests`) currently fail to build. XIHR emits a `DataTemplate`'s content as a stably-named `LoadTemplate_{line}_{pos}` local function so Edit-and-Continue has a stable anchor (#36482). That function was **hoisted** to the top of the generated method via `AddLocalMethod`, which breaks in two ways once XIHR is actually enabled: - **Duplicate method (CS0128 / CS8321).** A template value set more than once in the same scope — e.g. a `required` `DataTemplate` property, which the generator sets both in the object initializer *and* as an assignment — emitted the named function **twice** → `error CS0128: 'LoadTemplate_L_P' is already defined` (+ `CS8321` unused). - **Out-of-scope references (CS0103 / CS1503).** Hoisting to the method top **lost the lambda's lexical scope**, so a template body that referenced enclosing locals (the `DataTemplate` variable, name scopes, resources) generated references to names that don't exist at that scope → `CS0103`, with a cascading `CS1503`. These stayed latent because MAUI ships XIHR **opt-in (default off)**, so no repo project built through this path until default-on was attempted. ### Fix Emit the named local function **inline at the point of use** (not hoisted), which restores the exact lexical scope the anonymous lambda had, and **reserve each method name once per compilation unit** so it is declared a single time — every set-site just re-points `LoadTemplate` at that one function. Also removes ~25 lines of buffering. Non-HR builds are unchanged (still an anonymous lambda). ### Tests - New regression test `DataTemplate_HotReload_SetMultipleTimes_EmitsSingleNamedMethod` — a `required` `DataTemplate` property under XIHR. Verified it **fails without the fix** (`CS0128 'LoadTemplate_9_18' already defined`) and **passes with it**. - Full `SourceGen.UnitTests` suite green (**452/452**), including all existing #36482 XIHR tests. - Verified locally that `Controls.Xaml.UnitTests` builds with `EnableMauiIncrementalHotReload=true` with no `LoadTemplate` errors (the CS0128/CS8321 pair is gone). ### Related - Prerequisite for #36682 (enable XIHR by default in Debug). - Follow-up hardening of #34338 / #36482. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
XAML Incremental Hot Reload (XIHR, #34338) shipped opt-in behind EnableMauiIncrementalHotReload, defaulting to false everywhere. Per the .NET 11 Hot Reload plan (Phase 3, Preview 7), turn it on by default. The property now defaults to true for Debug builds and stays false for Release/publish, so the per-page registry calls and the Microsoft.Maui.RuntimeFeature.IsIncrementalHotReloadEnabled runtime switch trim away in shipped apps. This mirrors the existing EnableMauiXamlDiagnostics Debug-gating pattern in the same file. Opt out with <EnableMauiIncrementalHotReload>false</...>; legacy XAML Hot Reload remains available as a fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
<!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Description of Change Restores the Sandbox sample files modified by #34338 to their pre-PR state: - Removes the Incremental XAML Hot Reload demo UI and code-behind. - Removes the sample-only `EnableMauiIncrementalHotReload` opt-in. The source-generated patch-chain implementation remains unchanged. These Sandbox demo changes were not intended to ship. ## Testing The three restored files exactly match commit `7f139ed4175da5ef62ac0bd3e14791e6347b7ea8`, the parent of #34338's squash commit. No later commits changed these files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
<!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description XAML Incremental Hot Reload (XIHR) shipped in #34338 as **opt-in**, with `EnableMauiIncrementalHotReload` defaulting to `false` everywhere. Per the .NET 11 Hot Reload plan (Phase 3 — Default-On, Preview 7), this PR **turns it on by default**. ### What changed A single MSBuild default in `Microsoft.Maui.Controls.targets`: - `EnableMauiIncrementalHotReload` now defaults to **`true` for `Debug` builds** and stays **`false` for `Release`/publish**. - As a result, `MauiXamlHotReload` resolves to `SourceGen` (instead of `Legacy`) for Debug builds. Hot reload is a dev-time feature: gating default-on to `Debug` keeps the per-page registry calls and the `Microsoft.Maui.RuntimeFeature.IsIncrementalHotReloadEnabled` runtime switch **off in shipped apps**, so they trim away in `Release`/publish (the `RuntimeHostConfigurationOption` is emitted with `Trim="true"`). This mirrors the existing `EnableMauiXamlDiagnostics` Debug-gating pattern a few lines below in the same file. Users can still **opt out** with `<EnableMauiIncrementalHotReload>false</EnableMauiIncrementalHotReload>`; legacy XAML Hot Reload remains available as a fallback (VS / VS Code toggle). ### Behavior matrix | Configuration | `EnableMauiIncrementalHotReload` | `MauiXamlHotReload` | |---|---|---| | Debug (default) | `true` | `SourceGen` | | Release / publish (default) | `false` | `Legacy` | | Debug + explicit `false` | `false` | `Legacy` | | Release + explicit `true` | `true` | `SourceGen` | ### Testing Validated the MSBuild default cascade across all of the above scenarios (Debug→on/SourceGen, Release→off/Legacy, empty config→off, explicit opt-out/opt-in both respected). No public API changes; templates don't set the flag, so new projects pick up the Debug default automatically. Existing SourceGen unit tests (which set the flag per-file) continue to cover both on and off code-generation paths. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Description
This PR adds XAML Incremental Hot Reload (XIHR) — a Roslyn source generator + runtime feature that lets live XAML edits update existing instances without rebuilding the app. When the developer edits a XAML file, the generator emits a per-version patch method (
UpdateComponent) and the[MetadataUpdateHandler]runtime applies it to every live instance of the affected page, advancing each instance through its accumulated patch chain.The feature is opt-in via project property (
<EnableIncrementalHotReload>) and gated by aRuntimeFeatureswitch (IsIncrementalHotReloadEnabled) so trimmed/AOT production builds pay zero cost.What changed
Source generator (
src/Controls/src/SourceGen/):XamlGeneratororchestrates per-file IC + UC emission with versioning.InitializeComponentCodeWriteremits the initial__versionfield andRegister(...)calls.UpdateComponentCodeWriteremits per-version patch bodies (if (__version == N) { …; __version = N+1; }), supporting property changes, child add/remove, structural reorders, attached properties, markup extensions, bindings, and ResourceDictionary updates.XamlNodeDiffcomputes the semantic diff used to decide patch vs. structural reset vs. empty (no-op) diff.XamlHotReloadStatekeeps the per-file(prev XAML, parsed tree, node IDs, version, patch bodies)cache across generator invocations.Runtime (
src/Controls/src/Xaml/):XamlComponentRegistrytracks live instances + named components per page viaConditionalWeakTableand weak references.XamlIncrementalHotReloadHandler([assembly: MetadataUpdateHandler]) snapshots the registry on a metadata update, then dispatchesUpdateComponent()calls on the UI thread.Feature switch:
RuntimeFeature.IsIncrementalHotReloadEnabledwith[FeatureSwitchDefinition]+[FeatureGuard]so the trimmer can dead-strip the runtime when disabled.Sample:
Maui.Controls.Sample.Sandboxdemonstrates the developer scenario.Tests
XamlComponentRegistry(registration lifecycle, weak-ref cleanup, prefix rename).NoOpEdit_BetweenPatches_PreservesVersionChain— semantic no-op edits (e.g., adding a comment) must NOT reset version or clear accumulated patches.Review history
This branch went through 5 rounds of multi-model code review (Claude Sonnet 4.6, Claude Opus 4.7, GPT-5.5 in parallel). 7 round-1 blockers + 13 majors + 3 round-2 findings + 7 round-3 findings + 4 round-4 findings were all addressed. Round-5 verdict: LGTM from all 3 reviewers, high confidence.
Known follow-ups (not blockers for this PR)
XamlHotReloadState) keyed by(assembly, TFM, relativePath). A future refactor to pureIncrementalValueProviderpipelines would reduce coupling and improve build-server cacheability.XamlHotReloadStateaccumulates one entry per XAML file ever seen by the generator host. For multi-hour IDE sessions with file renames or deletes, entries are never reclaimed until generator-host shutdown. Adding pruning from the currentAdditionalTextssnapshot is a small follow-up.Targeting
Base branch:
net11.0(this is a new feature, not a bug fix).