Source: Loop document "Xaml (incremental) HotReload on XSG"
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.
| 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 |
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
When a XAML file changes, instead of reloading the whole page, the source generator:
- Diffs the old and new XamlNode trees (in Roslyn, not at runtime)
- Generates an
UpdateComponent()method alongsideInitializeComponent()that applies only the changed properties - The
[MetadataUpdateHandler]invokesUpdateComponent()on existing instances
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.
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
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.
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.
If anything goes wrong during UpdateComponent() generation or execution, the MetadataUpdateHandler MUST fall back to a full-page reload:
- Clear the BindingContext on the page
- Re-run
InitializeComponent() - Re-apply the BindingContext
This ensures the user always gets a working UI even if the incremental path fails.
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 fromInitializeComponent()for each named/referenced elementGetComponent(string nodeId)— called fromUpdateComponent()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.
The [MetadataUpdateHandler] attribute marks the class that C# Hot Reload invokes when types change. The handler will:
- Detect which pages/controls have a new
UpdateComponent()method - For each live instance of the updated type, call
UpdateComponent() - On any exception, fall back to full-page reload (see Fallback Behavior)
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)
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)
| # | 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 |
-
UpdateComponent()is generated alongsideInitializeComponent()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
- Error UX: How should
UpdateComponent()exceptions be reported in Visual Studio? - Node ID stability: What constitutes a "stable" node ID across edits? (position,
x:Name, or a generated hash?) - Custom controls: How does
RegisterComponenthandle controls that overrideInitializeComponent? - Partial pages /
ContentView: DoesUpdateComponent()need to propagate into embedded custom controls?