Skip to content

[XAML] Incremental XAML Hot Reload (source-generated patch chains) - #34338

Merged
StephaneDelcroix merged 24 commits into
net11.0from
feature/xaml-incremental-hotreload
Jul 20, 2026
Merged

[XAML] Incremental XAML Hot Reload (source-generated patch chains)#34338
StephaneDelcroix merged 24 commits into
net11.0from
feature/xaml-incremental-hotreload

Conversation

@StephaneDelcroix

@StephaneDelcroix StephaneDelcroix commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

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 a RuntimeFeature switch (IsIncrementalHotReloadEnabled) so trimmed/AOT production builds pay zero cost.

What changed

  • Source generator (src/Controls/src/SourceGen/):

    • XamlGenerator orchestrates per-file IC + UC emission with versioning.
    • InitializeComponentCodeWriter emits the initial __version field and Register(...) calls.
    • UpdateComponentCodeWriter emits 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.
    • XamlNodeDiff computes the semantic diff used to decide patch vs. structural reset vs. empty (no-op) diff.
    • XamlHotReloadState keeps the per-file (prev XAML, parsed tree, node IDs, version, patch bodies) cache across generator invocations.
  • Runtime (src/Controls/src/Xaml/):

    • XamlComponentRegistry tracks live instances + named components per page via ConditionalWeakTable and weak references.
    • XamlIncrementalHotReloadHandler ([assembly: MetadataUpdateHandler]) snapshots the registry on a metadata update, then dispatches UpdateComponent() calls on the UI thread.
  • Feature switch: RuntimeFeature.IsIncrementalHotReloadEnabled with [FeatureSwitchDefinition] + [FeatureGuard] so the trimmer can dead-strip the runtime when disabled.

  • Sample: Maui.Controls.Sample.Sandbox demonstrates the developer scenario.

Tests

  • 414 SourceGen unit tests covering: foundation, IC/UC emission, all patch shapes, structural diff, ResourceDictionary handling, registry round-trips, full E2E pipeline (multi-run scenarios).
  • 26 runtime tests for XamlComponentRegistry (registration lifecycle, weak-ref cleanup, prefix rename).
  • Notable regression test: 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)

  • True IncrementalGenerator purity: The current implementation uses static mutable state (XamlHotReloadState) keyed by (assembly, TFM, relativePath). A future refactor to pure IncrementalValueProvider pipelines would reduce coupling and improve build-server cacheability.
  • Long-lived IDE session cache pruning: XamlHotReloadState accumulates 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 current AdditionalTexts snapshot is a small follow-up.

Targeting

Base branch: net11.0 (this is a new feature, not a bug fix).

Copilot AI review requested due to automatic review settings March 4, 2026 20:01
@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34338

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34338"

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread src/Controls/tests/SourceGen.UnitTests/XamlIncrementalHotReloadPipelineTests.cs Outdated
Comment thread src/Controls/src/SourceGen/UpdateComponentCodeWriter.cs Outdated
Comment thread src/Controls/src/SourceGen/UpdateComponentCodeWriter.cs Outdated
Comment on lines +155 to +169
// 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;");
}

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/Controls/src/SourceGen/XamlNodeDiff.cs Outdated
@StephaneDelcroix
StephaneDelcroix marked this pull request as draft March 4, 2026 21:20
@StephaneDelcroix
StephaneDelcroix force-pushed the feature/xaml-incremental-hotreload branch from 8f1a3cc to 459e08c Compare March 11, 2026 08:11
@StephaneDelcroix
StephaneDelcroix force-pushed the feature/xaml-incremental-hotreload branch 2 times, most recently from 1e2557d to 9c34d31 Compare March 28, 2026 07:45
@kubaflo

kubaflo commented May 24, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/refactor-copilot-yml

@kubaflo

kubaflo commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

AI code review for net11.0 target

Verdict: 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 XamlComponentRegistry, and feature-switch plumbing. High-level pass, not an exhaustive audit.

Observations:

  • Properly feature-gated. RuntimeFeature.IsIncrementalHotReloadEnabled defaults to false, is wired via [FeatureSwitchDefinition] (NET10+) and the EnableMauiIncrementalHotReload MSBuild prop, and the handler early-returns when disabled. Good trimming/AOT hygiene and safe opt-in.
  • New public XamlComponentRegistry API (Register/TryGet/Unregister/GetInstances/ReRoot) is reasonably public since generated InitializeComponent()/UpdateComponent_vNtoM() in user assemblies must call it. Args are null-checked.
  • Registry is documented as keyed on the page instance and holding components — please confirm the lifetime story (weak references / Unregister on teardown) so live-instance tracking can't leak pages across hot-reload generations. The XML docs hint at weak holding; worth verifying in code review.
  • Structural-change → full-reload fallback (vs property-only diff) is a sensible safety valve. Test coverage is substantial (diff, pipeline, E2E, node-id helpers).

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.

@StephaneDelcroix
StephaneDelcroix force-pushed the feature/xaml-incremental-hotreload branch 2 times, most recently from 09607a6 to f276fc7 Compare June 10, 2026 14:25
@kubaflo

kubaflo commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

AI code review refresh for net11.0 target

Head reviewed: f276fc7c0e2cc58522032f406acc66822134c859 (the fleet scan's cbaee97… is stale; trusting actual PR head)
Target branch: net11.0 · State: open, draft/WIP
Verdict: Needs changes — the design feedback from the prior round is well addressed, but this PR's own new tests fail on Windows CI for two concrete, fixable reasons.

Prior-review reconciliation (round-15 → now)

The new commit f276fc7 explicitly closes my earlier open items:

  • Registry lifetime/leakXamlComponentRegistry now stores component maps and resource keys in ConditionalWeakTable, and the GetInstances secondary index uses WeakReference<object> with dead-entry pruning on each register. Pages GC freely; no strong-rooting. Concern resolved.
  • Double-bookkeeping (M10) — the handler's parallel weak list was dropped; UpdateApplication now queries GetInstances(type) directly. Track() retained as a no-op for already-compiled IC bodies (good backward-compat instinct).
  • Public API review (B2/M3) — all 7 PublicAPI.Unshipped.txt files aligned; XamlComponentRegistry marked [EditorBrowsable(Never)].
  • Determinism/AOT (M1/M2/M4)[FeatureGuard] on the runtime switch, \n + sorted HashSet emission for bit-deterministic codegen, SymbolDisplay.FormatLiteral for resource-key injection safety. The B1–B5/M1–M13 changelog is thorough and credible.

CI status (build 1457900)

Mixed. Classification:

  • 🔴 Windows Helix Microsoft.Maui.Controls.SourceGen.UnitTests (Debug+Release)PR-relevant, actionable. 17 failures, two root causes:
    1. 11× XamlNodeDiffTests.ToDebugString_* — CRLF≠LF assertion mismatches (Expected "…\n…", Actual "…\r\n…"). The codegen was made to emit \n, but ToDebugString / these fixtures still surface Environment.NewLine on Windows. Normalize line endings in ToDebugString (or the assertions) so the suite is host-OS-independent — this is exactly why a macOS-only local run reports green.
    2. XamlIncrementalHotReloadE2ETests.*MetadataUpdater.IsSupported is false … set <MetadataUpdaterSupport>true> / DOTNET_MODIFIABLE_ASSEMBLIES=debug. These E2E tests require runtime metadata-update support that isn't enabled on the Helix runner (and is off in Release). Guard them with a skip when MetadataUpdater.IsSupported == false, or ensure the test project sets the required property/env in all CI configs.
  • macOS Release build (error CA1416 in src/Graphics/.../iOS/UIImageExtensions.cs)unrelated to this PR (file not touched); pre-existing/base-branch analyzer breakage.
  • 🟡 AOT macOS/windows + RunOniOS TrimFull — red, but reported as harness-level (failedTests:0, "Test suite had 1 failure(s)"); not clearly attributable here. Given trim-safety is a stated design goal, please confirm these are not trimming regressions from the gated paths before un-drafting.

Blast radius

  • Runtime: opt-in only — RuntimeFeature.IsIncrementalHotReloadEnabled defaults false, [FeatureGuard]/[FeatureSwitchDefinition] gated, handler early-returns when off, registry [EditorBrowsable(Never)]. Negligible risk to users who don't enable XIHR.
  • Build/codegen: the generated InitializeComponent() body changes run for every XAML compile, so codegen determinism/correctness matters broadly — the \n/sorted-emission work is the right mitigation; the failing Windows tests are the guardrail catching residual host-dependence.

Findings summary

  • Must-fix before merge: the two Windows SourceGen test causes above (line-ending normalization; MetadataUpdater.IsSupported skip-guard).
  • Recommend: confirm AOT/TrimFull reds are not feature-related; keep the registry/Track() no-op compatibility note in the changelog.

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.

@StephaneDelcroix
StephaneDelcroix force-pushed the feature/xaml-incremental-hotreload branch 3 times, most recently from 2d9c193 to 7f0c8ee Compare June 11, 2026 06:58
@StephaneDelcroix StephaneDelcroix changed the title [Feature] XAML Incremental Hot Reload via Source Generator [XAML] Incremental XAML Hot Reload (source-generated patch chains) Jun 11, 2026
@StephaneDelcroix
StephaneDelcroix marked this pull request as ready for review June 11, 2026 09:40
@kubaflo

kubaflo commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

AI code review refresh for net11.0 target

Head reviewed: 7f0c8eee3dad3f281b19c05ef05825530ff76011
Target branch: net11.0 · State: open · Author: area owner (XAML/SourceGen)
Verdict: Needs changes — most round‑16 items are now resolved and the new commit is high‑quality, but the round‑16 line‑ending (CRLF/LF) failure in the ToDebugString_* tests still appears unaddressed at this head, and the head's Helix unit run has not yet completed.

Note: the branch was rebased since round‑16 (the previously reviewed f276fc7… is no longer in history; the current tip squashes the work into 7 commits ending at 7f0c8ee). Reconciliation below is against round‑16's two must‑fix items, evaluated against current head content.

Prior‑review reconciliation (round‑16 → now)

  • M‑updater E2E failures (MetadataUpdater.IsSupported is false) — resolved. SourceGen.UnitTests.csproj now sets <MetadataUpdaterSupport>true</MetadataUpdaterSupport> (with the explanatory comment), which makes IsSupported true under CoreCLR even in Release. The E2E helper's hard Assert.True(MetadataUpdater.IsSupported, …) should now pass.
  • CRLF/LF in XamlNodeDiff.ToDebugString testsnot fixed at head. ToDebugString() still builds output with sb.AppendLine() (→ Environment.NewLine = \r\n on Windows), while the 11+ XamlNodeDiffTests.ToDebugString_* cases assert literal "…\n…" via plain Assert.Equal. I grepped the test file at head: 0 occurrences of ReplaceLineEndings/Environment.NewLine/.Replace( — no normalization anywhere. The author's "All 413 SourceGen unit tests pass" reflects a macOS run (where NewLine == "\n"); this is exactly the host‑dependence round‑16 flagged and it will re‑fail on the Windows Helix leg. Fix: either emit '\n' explicitly in ToDebugString (e.g. sb.Append('\n') instead of AppendLine()), or normalize in the assertions.
  • Design items B1–B5 / M1–M13 — the changelog is thorough and the head bears it out: [FeatureGuard] switch, deterministic \n+sorted‑HashSet codegen (XamlNodeDiff.DiffChildrenWithMatching now sorts keys Ordinal), SymbolDisplay.FormatLiteral for resource‑key + string‑literal safety, registry [EditorBrowsable(Never)], all 7 PublicAPI.Unshipped.txt aligned, Track() reduced to a null‑guarded no‑op, registry instance index sourced from GetInstances.

New since round‑16 (this commit) — spot review

  • Handler now actually wired: [assembly: MetadataUpdateHandler(typeof(XamlIncrementalHotReloadHandler))] is uncommented and the type is now public [EditorBrowsable(Never)]. UpdateApplication still early‑returns when IsIncrementalHotReloadEnabled == false, so the live hot‑reload path remains opt‑in. M9 batch dispatch (single BeginInvokeOnMainThread) and M10 (registry‑sourced instances) look correct.
  • AOT/trim hazard removed (good): EmitContentPropertyChange no longer emits ((dynamic)parent).Content = … unconditionally — it emits a typed cast when the parent type resolves and only falls back to dynamic otherwise. This removes a Microsoft.CSharp dependency that is incompatible with NativeAOT/full trim. Sensible given the always‑on codegen path.
  • Event‑handler churn: new OldValue on PropertyDiff drives unsubscribe‑old/subscribe‑new, guarded by IsValidCSharpIdentifier. x:DataType‑change detection on the node itself (M7, DetectXDataTypeChange) correctly forces same‑node binding refresh. Logic reads sound.

CI status

  • 🟠 Windows Helix Unit Tests — the run linked from gh pr checks (build 1459135) was canceled/superseded when the current build was queued; its fail status is not a clean signal. The live build 1459322 is in progress and its Helix unit tests have not run yet, so there is no green Windows SourceGen result for this head. Combined with the unfixed ToDebugString CRLF mismatch, I expect the Windows SourceGen leg to re‑fail.
  • 🟡 AOT macOS/windows + RunOniOS TrimFull/CoreCLR — red in 1459322, but the AOT macOS log surfaces MSB4276 SDK‑resolver/harness noise rather than a feature‑specific compile error; consistent with round‑16's "harness‑level, not clearly attributable" classification. The feature is gated off by default and the dynamic→typed‑cast change is the right mitigation. Please confirm these are the usual pre‑existing reds and not a trim regression from the always‑on codegen before un‑gating.
  • ✅ Windows/macOS Debug+Release builds, Pack, and the bulk of integration legs (Blazor, MultiProject, Samples, RunOnAndroid, most RunOniOS) are green.

Blast radius

  • Runtime: opt‑in (RuntimeFeature.IsIncrementalHotReloadEnabled defaults false, [FeatureGuard], handler early‑returns, registry [EditorBrowsable(Never)]). Low risk for users who don't enable XIHR — but note the [assembly: MetadataUpdateHandler] is now always present, so the runtime will invoke UpdateApplication during metadata updates; the feature‑switch early‑return is the only behavioral gate. Worth a sanity check that a disabled‑XIHR app sees zero behavioral change under dotnet‑watch.
  • Build/codegen: the generated InitializeComponent()/UpdateComponent() emission runs for every XAML compile, so determinism/correctness is broad‑impact. The \n+sorted‑key work and FormatLiteral escaping are the right guards; the Windows test leg is the guardrail catching the residual ToDebugString host‑dependence.

Findings summary

  • Must‑fix: ToDebugString line endings (emit '\n' or normalize the asserts) so the Windows SourceGen suite is host‑independent.
  • Confirm before un‑gating: AOT/TrimFull reds are pre‑existing harness noise, not a trim regression from the gated/codegen paths; and a disabled‑XIHR app is behaviorally inert despite the now‑active [MetadataUpdateHandler].
  • Nice: the AOT dynamic removal, deterministic codegen, and registry/Track() simplification are solid improvements.

Confidence: high on the ToDebugString CRLF diagnosis (code + test read at head; 0 normalization sites) and on the MetadataUpdater resolution; medium on CI (current Helix run incomplete, prior build canceled) and on the AOT reds (not deep‑dived).

Automated non‑approval review. No human approval is implied or given; this comment does not gate merge and uses neither approve nor request‑changes.

@StephaneDelcroix
StephaneDelcroix force-pushed the feature/xaml-incremental-hotreload branch from 7f0c8ee to b260645 Compare June 12, 2026 09:21
@kubaflo

kubaflo commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

AI code review refresh for net11.0 target

Head reviewed: b260645ab9fa984384808a21165c3598071e2c5e
Target branch: net11.0 · State: open · Author: area owner (XAML/SourceGen)

Verdict: Needs changes — the new commit (b260645, "handler optimization, IC fallback gate, cleanup, and rev…") is genuinely good work and lands several correctness/trim wins, but two distinct SourceGen-unit-test failures are reproducible at this exact head, one of which I previously (round 17) believed resolved and is in fact still failing — on both platforms now.

Prior-review reconciliation (round 17 → now)

  • CRLF/LF in XamlNodeDiff.ToDebugString tests — STILL NOT FIXED. ToDebugString() still uses sb.AppendLine() (→ Environment.NewLine = \r\n on Windows) at XamlNodeDiff.cs (lines ~189, ~208), while the 11 XamlNodeDiffTests.ToDebugString_* cases assert literal "…\n…" via plain Assert.Equal. CI confirms it empirically: Windows Debug run 40404320 fails e.g. ToDebugString_DeeplyNested with Expected: …\n… / Actual: …\r\n…. Fix: emit '\n' explicitly in ToDebugString (sb.Append('\n') instead of AppendLine()), or normalize in the asserts.
  • MetadataUpdater.IsSupported is false (E2E) — REGRESSION vs my round-17 ✅. I marked this resolved in round 17 by inferring from the <MetadataUpdaterSupport>true</MetadataUpdaterSupport> csproj setting; the Windows Helix leg hadn't finished, so that ✅ was premature. With CI now complete, 5 XamlIncrementalHotReloadE2ETests.*_AppliedViaHotReload / ChainedPatches tests fail on BOTH macOS (40404322) and Windows (40404320) at AssertHotReloadSupported() (E2ETests.cs:209). The csproj flag is present at head but is not sufficient in the xUnit/Helix runner — on CoreCLR MetadataUpdater.IsSupported also requires the process to start with modifiable assemblies (DOTNET_MODIFIABLE_ASSEMBLIES=debug) / hot-reload-enabled launch. Fix: set that env var for the test run (runsettings/RuntimeHostConfigurationOption / Helix payload env), or make the E2E helper Skip when unsupported instead of Assert.True.
  • ✅ Deterministic \n+sorted-key codegen, SymbolDisplay.FormatLiteral escaping, registry [EditorBrowsable(Never)], all 7 PublicAPI.Unshipped.txt, dynamic→typed-cast AOT mitigation, [MetadataUpdateHandler] wiring + feature-switch early-return — all hold at head.

New since round 17 (spot review of b260645) — solid

  • TFM now part of the hot-reload state cache key (GetParsedRoot/GetNodeIds/GetVersion(asm, tfm, relPath)) — correctly prevents cross-TFM state collision in multi-targeted builds. Good catch.
  • emptyDiff out-param with an excellent doc-comment: formatting/comment-only edits no longer reset the version chain (avoids stranding live instances at if (__version == 0)). Sound.
  • FormatLiteral for resource keys, EnableDiagnostics codegen gated off when XIHR is enabled, RuntimeFeature [FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))] for trim correctness. All reasonable.

CI status (maui-pr build 1461045 — FAILED)

  • Windows Helix Unit Tests (Debug + Release) — fail. Debug leg = 17 failures: 11 × ToDebugString_* (CRLF) + 5 × E2E (IsSupported) + the SourceGen.UnitTests.dll work-item crash that follows.
  • macOS Unit Tests — the 5 E2E IsSupported failures reproduce here too (no ToDebugString failures on mac since NewLine == "\n"), confirming the MetadataUpdater issue is platform-independent, not Windows-only.
  • 🟡 AOT macOS/windows + RunOniOS TrimFull/CoreCLR — red, but the AOT macOS log (logId 1101) surfaces MSB4276 SDK-resolver noise (sdk/10.0.100/Sdks/... did not exist), i.e. harness/provisioning, not a feature compile error. Consistent with the usual pre-existing reds; the gated + typed-cast codegen is the right mitigation.
  • ✅ Windows/macOS Debug+Release builds, Pack, and the bulk of integration legs (templates, Blazor, MultiProject, Samples, RunOnAndroid, most RunOniOS) green.

Blast radius / failure-mode probing

  • Codegen runs for every XAML compile — TFM-keying and the diagnostics gate are broad-impact; the deterministic \n/sorted-key + FormatLiteral guards are the right protection. The two failing test buckets are test-side (host line-endings + runner hot-reload capability), not user-facing codegen correctness — but they block the SourceGen leg and must go green before merge.
  • Runtime stays opt-in (IsIncrementalHotReloadEnabled default false, [FeatureGuard], handler early-return). The [assembly: MetadataUpdateHandler] is always present, so UpdateApplication is invoked during metadata updates; the feature-switch early-return is the only behavioral gate — worth a final sanity check that a disabled-XIHR app under dotnet-watch is behaviorally inert.

Findings summary

  • Must-fix [Draft] Readme WIP #1: ToDebugString line endings (emit '\n' / normalize asserts) → host-independent Windows SourceGen leg.
  • Must-fix Update README.md #2: make MetadataUpdater.IsSupported true in the test runner (DOTNET_MODIFIABLE_ASSEMBLIES=debug env in the Helix/runsettings payload) or Skip the E2E tests when unsupported — currently failing on both platforms.
  • Confirm: AOT/TrimFull reds are the usual harness/SDK-resolver noise, not a trim regression from the always-on codegen.

Confidence: high on both must-fix items — they are reproduced by the completed CI test runs at this exact head (Windows 40404320, macOS 40404322) and corroborated by reading the code/tests at head; high on the AOT harness classification (MSB4276 confirmed in logId 1101). I explicitly correct my round-17 ✅ on MetadataUpdater: that was premature.

Automated non-approval review. No human approval is implied or given; this comment does not gate merge and uses neither approve nor request-changes.

@kubaflo

kubaflo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

PR #34338 — [XAML] Incremental XAML Hot Reload (source-gen)

Verdict: NEEDS_DISCUSSION (confidence: medium). This is a large, sophisticated dev-time feature (a generated UpdateComponent that diffs the XAML node tree and applies only changed properties to live instances). The core infrastructure reviewed well — gemini independently found the XamlComponentRegistry thread-safe, GC-tolerant (weak storage), and dispatching patches correctly to the UI thread, gated behind RuntimeFeature. Flagging for human/maintainer judgment because (a) a couple of hot-reload-flow edge cases are worth confirming, (b) a CI leg looks PR-relevant, and (c) 2 of the 4 review models didn't finish, so model coverage is partial.

Worth confirming (hot-reload flow — dev-time behavior, not production)

  1. Structural edits and already-live pages (XamlGenerator.cs:263-273). When a change is classified as structural (XamlNodeDiff can't express it as a property patch), this branch updates the cached tree + clears patches but doesn't emit an UpdateComponent patch or otherwise route already-live instances through a full reload. Since InitializeComponentCodeWriter suppresses the runtime ResourceLoader fallback when incremental HR is enabled, please confirm that existing live pages still get the new tree after add/remove/type-change edits (new instances clearly do). If structural edits are intended to fall back to a full reload, verify that path isn't suppressed for live instances.
  2. Root-only edits (XamlIncrementalHotReloadHandler.cs:42). Live-instance enumeration relies on XamlComponentRegistry.Register being called for at least one non-root component, and InitializeComponent skips the root node id / only registers non-empty ids. Confirm that a page whose XAML only changes root properties/resources (no registered child component) still enters GetInstances/UpdateComponent — otherwise root-only edits won't hot-reload.
  3. (minor) NodeIdHelper.AssignChildrenRecursive only traverses parent.CollectionItems — gemini notes it may miss children held in non-collection properties; worth a glance for completeness of node identity.

⚠️ CI

Run Helix Unit Tests Windows (Debug & Release) is failing on this head (build 1461045). The same leg is green on non-SourceGen PRs but also red on the sibling SourceGen PR #33561 — so this looks PR-relevant (likely the new SourceGen.UnitTests for the hot-reload codegen) rather than the usual flake. Please confirm whether the new source-gen unit tests are passing. (AOT macOS/windows, RunOniOS_*Trim* are the known unrelated flakes.)

Note

opus-4.8 and opus-4.6 returned only placeholders this run (didn't complete on a 12k-line PR); this synthesis is based on gpt-5.5 + gemini + direct code/CI inspection. A re-review will get fuller model coverage on the next push. Overall: promising feature with solid threading/registry foundations — the open items are flow-completeness + the CI check.

Multi-model review (gpt-5.5 · opus-4.8 · opus-4.6 · gemini-3.1-pro). Comments only — not a formal approval.

@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jun 18, 2026
StephaneDelcroix and others added 13 commits July 20, 2026 09:05
- 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>
Copilot AI review requested due to automatic review settings July 20, 2026 07:33
@StephaneDelcroix
StephaneDelcroix force-pushed the feature/xaml-incremental-hotreload branch from d434913 to 1f3232b Compare July 20, 2026 07:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ... and set ... as separate HelixPreCommands and assumes the non-native command is a harmless no-op. In practice, set DOTNET_MODIFIABLE_ASSEMBLIES=debug in bash is not a no-op (it changes positional parameters), and depending on how Helix executes pre-commands, the failing export on 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 passing ProjectItem.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">

Comment on lines +42 to +51
// 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}";
}
Comment on lines +252 to +255
if (Context.ProjectItem.EnableIncrementalHotReload)
{
var methodName = TemplateLoadMethodName(node);

@StephaneDelcroix StephaneDelcroix added this to the .NET 11.0-preview7 milestone Jul 20, 2026
@StephaneDelcroix
StephaneDelcroix merged commit 293cadb into net11.0 Jul 20, 2026
32 checks passed
@StephaneDelcroix
StephaneDelcroix deleted the feature/xaml-incremental-hotreload branch July 20, 2026 12:42
StephaneDelcroix added a commit that referenced this pull request Jul 22, 2026
…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>
StephaneDelcroix added a commit that referenced this pull request Jul 22, 2026
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>
PureWeen pushed a commit that referenced this pull request Jul 22, 2026
<!-- 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>
kubaflo pushed a commit that referenced this pull request Jul 23, 2026
<!-- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants