Skip to content

Instantly share code, notes, and snippets.

@StephaneDelcroix
Created March 4, 2026 14:39
Show Gist options
  • Select an option

  • Save StephaneDelcroix/9bbe3620ee46ea09d32a53e327f3f9c0 to your computer and use it in GitHub Desktop.

Select an option

Save StephaneDelcroix/9bbe3620ee46ea09d32a53e327f3f9c0 to your computer and use it in GitHub Desktop.
XAML Incremental Hot Reload via XSG - Feature Spec

Spec: XAML Incremental Hot Reload via XSG (XAML Source Generation)

Source: Loop document "Xaml (incremental) HotReload on XSG"


Objective

Leverage Roslyn XAML Source Generation (XSG) to implement incremental XAML Hot Reload (XHR) by deferring work to C# Hot Reload. This improves consistency, testability, supportability, and performance, and reduces maintenance overhead.


Background & Motivation

Current State (without this feature)

Scenario XSG enabled Behavior
Full-page Hot Reload Yes Works via navigation (no stable MetadataUpdateHandler on Mono yet)
Incremental Hot Reload Yes Both full-page and incremental XHR work reliably
Incremental Hot Reload No (Runtime/XamlC) Handled by the existing XHR pipeline

Problem

The existing XAML Hot Reload pipeline is separate from C# Hot Reload. By integrating XSG with incremental HotReload, we can:

  • Use Roslyn for diffing (more robust than runtime diffing)
  • Generate precise, targeted update code instead of doing full-page re-initialization
  • Eliminate full-page flicker on small XAML edits
  • Unify the full-page and incremental reload experience under one path

Proposal: Incremental XHR via XSG

Concept

When a XAML file changes, instead of reloading the whole page, the source generator:

  1. Diffs the old and new XamlNode trees (in Roslyn, not at runtime)
  2. Generates an UpdateComponent() method alongside InitializeComponent() that applies only the changed properties
  3. The [MetadataUpdateHandler] invokes UpdateComponent() on existing instances

Example

Original XAML:

<ContentPage Title="My Page">
    <Label Text="Hello, World!" TextColor="HotPink" />
</ContentPage>

Generated InitializeComponent() (initial load):

private partial void InitializeComponent()
{
    var label = new Label();
    RegisterComponent(label, "unique-id");
    this.SetValue(Page.TitleProperty, "My Page");
    label.SetValue(Label.TextProperty, "Hello, World!");
    label.SetValue(Label.TextColorProperty, Colors.HotPink);
    this.SetValue(ContentPage.ContentProperty, label);
}

After XAML change (Text updated, BackgroundColor added, TextColor removed):

<ContentPage Title="My Page">
    <Label Text="Hello, MAUI!" BackgroundColor="Blue" />
</ContentPage>

Generated UpdateComponent() (incremental update):

internal void UpdateComponent()
{
    var label = (Label)GetComponent("unique-id");
    label.SetValue(Label.TextProperty, "Hello, MAUI!");
    label.SetValue(Label.BackgroundColorProperty, Colors.Blue);
    label.ClearValue(Label.TextColorProperty);   // unset removed property
}

The MetadataUpdateHandler calls UpdateComponent(), updating only the changed properties — no full-page flicker.


Incremental Update Goals

For each XAML edit, UpdateComponent() must:

  • Set newly added properties
  • Update changed property values
  • Clear (ClearValue) properties that were removed from XAML
  • Avoid full-page refresh for property-only changes

Diffing Strategy

Three strategies were considered:

Strategy Location Pros Cons Decision
Object Tree Diffing Runtime Direct access to live objects Requires object comparison; fragile for custom controls ❌ Not chosen
XAML Text Diffing Build/Runtime Simple to implement Fragile (whitespace/ordering sensitivity) ❌ Not chosen
XamlNode Tree Diffing Roslyn (build time) Robust, context-aware, leverages XSG knowledge Requires Roslyn integration Recommended

Decision: XamlNode diffing in Roslyn. The diff runs at build time inside the source generator, producing a precise list of property changes (set, update, clear) per component node.


Versioning Support

To handle multiple sequential edits correctly, each page tracks a version counter. UpdateComponent() applies incremental patches in order:

public partial class TestPage
{
    int __version = 0;

    private partial void InitializeComponent()
    {
        // initializes to version 2 state
        __version = 2;
    }

    internal void UpdateComponent()
    {
        if (__version == 0)
        {
            // patch: v0 → v1
            __version = 1;
        }
        if (__version == 1)
        {
            // patch: v1 → v2
            __version = 2;
        }
    }
}

This ensures an instance created on version 0 can be brought up to date even if multiple saves happened while it was alive.


Fallback Behavior

If anything goes wrong during UpdateComponent() generation or execution, the MetadataUpdateHandler MUST fall back to a full-page reload:

  1. Clear the BindingContext on the page
  2. Re-run InitializeComponent()
  3. Re-apply the BindingContext

This ensures the user always gets a working UI even if the incremental path fails.


Component Registry

UpdateComponent() needs to look up live objects by their XAML node identity. This requires a registry similar to the existing RegisterSourceInfo mechanism used by XHR:

  • RegisterComponent(object element, string nodeId) — called from InitializeComponent() for each named/referenced element
  • GetComponent(string nodeId) — called from UpdateComponent() to retrieve the live object

The nodeId is a stable identifier assigned by the source generator based on the node's position/identity in the XAML tree.


[MetadataUpdateHandler] Integration

The [MetadataUpdateHandler] attribute marks the class that C# Hot Reload invokes when types change. The handler will:

  1. Detect which pages/controls have a new UpdateComponent() method
  2. For each live instance of the updated type, call UpdateComponent()
  3. On any exception, fall back to full-page reload (see Fallback Behavior)

XHR Obsolescence

With this feature fully implemented:

  • The existing separate XAML Hot Reload pipeline becomes redundant
  • Full-page and incremental reload will both be driven through XSG + C# Hot Reload
  • The key behavioral difference from the old XHR: reload triggers on Save (not on any other event)

Tooling / UX Considerations

Exception Reporting

The old XHR pipeline caught reload exceptions and displayed them in Visual Studio. This feature must define equivalent behavior:

  • Open question: What is the desired UX when UpdateComponent() throws (e.g., ResourceNotFoundException)?
  • Candidates:
    • Display an error bar in the IDE (same as current XHR)
    • Silently fall back to full-page reload and log to Output window
    • Show an in-app overlay (like the current hot reload error overlay)

Key Components (Implementation Breakdown)

# Component Description
1 XamlNode Diff Engine Compares two XamlNode trees and produces a change set: properties to set, update, or clear per node
2 Component Registry Runtime mechanism to register/retrieve live objects by stable node IDs (RegisterComponent / GetComponent)
3 UpdateComponent() Generator Source generator emits UpdateComponent() from the diff result, applying versioned patches
4 [MetadataUpdateHandler] Hooks into C# Hot Reload to invoke UpdateComponent() on live page instances

Acceptance Criteria

  • UpdateComponent() is generated alongside InitializeComponent() for all XSG-inflated XAML pages
  • A XAML property change triggers UpdateComponent() on live instances without full-page reload
  • Removed XAML properties are correctly cleared via ClearValue()
  • Version counter ensures multi-step incremental patches apply correctly
  • On any UpdateComponent() failure, fallback to full-page reload completes successfully
  • Exception/error from UpdateComponent() is surfaced to the user in VS (UX TBD)
  • Reload triggers on Save (consistent with XHR behavior)
  • All existing XSG unit tests continue to pass

Open Questions

  1. Error UX: How should UpdateComponent() exceptions be reported in Visual Studio?
  2. Node ID stability: What constitutes a "stable" node ID across edits? (position, x:Name, or a generated hash?)
  3. Custom controls: How does RegisterComponent handle controls that override InitializeComponent?
  4. Partial pages / ContentView: Does UpdateComponent() need to propagate into embedded custom controls?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment