Skip to content

Build Reactor's DataTemplates from code instead of parsing markup - #1125

Draft
Alexandre Zollinger Chohfi (azchohfi) wants to merge 2 commits into
mainfrom
azchohfi-winui3-integration-datatemplate
Draft

Build Reactor's DataTemplates from code instead of parsing markup#1125
Alexandre Zollinger Chohfi (azchohfi) wants to merge 2 commits into
mainfrom
azchohfi-winui3-integration-datatemplate

Conversation

@azchohfi

@azchohfi Alexandre Zollinger Chohfi (azchohfi) commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Important

Draft — do not merge yet. This is blocked on a public Windows App SDK release that ships the code-based DataTemplate constructor. CI on this branch cannot pass: the versions in Directory.Build.props point at an internal experimental build that does not resolve from public feeds. Parked here so the change is ready to go the day the API ships.

What this does

Reactor builds two DataTemplates at runtime by handing XamlReader.Load a markup string:

  • SharedContentControlTemplate — the ContentControl shell every items control mounts (ListView / GridView / FlipView)
  • TreeViewTextItemTemplate — the legacy text-node TreeView item template

Both exist only because DataTemplate could not be constructed from code. microsoft-ui-xaml adds new DataTemplate(DataTemplateElementFactory), which takes a callback returning the subtree, so this ports both onto it and Reactor stops parsing XAML at runtime for item templates.

The part that is more than cosmetic

The TreeView template also loses its {Binding Content.Content}.

A classic {Binding} resolves that path by string, through CsWinRT's ICustomPropertyProvider — i.e. reflection. That means it is trimmed away under NativeAOT unless the source type carries generated binding metadata, and even where it works it costs a reflective lookup per realized row.

Reading the same chain in a DataContextChanged handler is strongly typed:

tb.DataContextChanged += static (sender, args) =>
{
    if (sender is not TextBlock text) return;
    text.Text = args.NewValue switch
    {
        TreeViewNode { Content: TreeViewNodeData d } => d.Content,
        TreeViewNode { Content: string s }           => s,
        TreeViewNodeData d                            => d.Content,
        _                                             => string.Empty,
    };
};

No metadata required, no reflective lookup, and the event re-fires when a virtualized row is recycled onto new data, so reused rows retext correctly.

x:Bind is not an alternative here — it is a XAML-compiler feature that generates code, so it is unavailable from a code-built subtree. DataContextChanged is the AOT-safe route, which matches the guidance in the upstream API discussion.

Removing the binding metadata attribute

#1108 annotated TreeViewNodeData with [WinRT.GeneratedBindableCustomProperty] for exactly one reason: to keep the {Binding Content.Content} above working under NativeAOT. This branch deletes that binding, so the attribute — and the partial it forced onto a public record — goes with it. After this change there is no {Binding} anywhere in src/ and no SetBinding call site.

It never shipped: v0.1.0-preview.13 and every earlier tag predate #1108, so no released build carries it, and binding to TreeViewNodeData from a consumer-supplied template was already broken under NativeAOT in all of them. Removing it takes away nothing that ever worked in a release.

The limitation is written down instead of silently dropped — docs/aot-support.md gains a row explaining that a classic {Binding} in a hand-supplied DataTemplate is not trim-safe, that Reactor's own templates avoid it by populating content in a DataContextChanged handler, and that a consumer installing their own template through an escape hatch such as TreeView(...).Set(tv => tv.ItemTemplate = ...) should do the same or annotate their own type.

Incidental: an ambiguous cref

The Windows App SDK this branch targets adds Microsoft.UI.Xaml.IElementFactory alongside the existing Microsoft.UI.Xaml.Controls.IElementFactory, which makes the bare <see cref="IElementFactory"/> in IItemsRepeaterFactorySource ambiguous — CS0419, an error under warnings-as-errors in Release. Disambiguated to the Controls one, which is what the member returns.

Validation

Validated against Microsoft.WindowsAppSDK.WinUI 3.0.0-experimental.260818.21 (the PR-built native core, Microsoft.ui.xaml.dll 3.3.0.2608):

  • src/Reactor Release build: 0 warnings, 0 errors
  • Full selftest suite: 1447 planned, only the known WindowLevel_RuntimeFlip_Topmost Z-order flake failing under full-suite load (passes 3/3 in isolation, unrelated to these files)
  • TXB_ (text-node TreeView), TTV_ (templated TreeView) and KLR_ (keyed ListView/GridView/ItemsRepeater) all green — mount, update and recycling arms
  • Earlier validation of this approach on an AOT-published host confirmed subtree creation, DataContext propagation and element recycling behave identically to JIT

Unblocking checklist

When the API ships in a public Windows App SDK:

  • Update WindowsAppSDKVersion / WindowsAppSDKWinUIVersion in Directory.Build.props to the released versions and delete the placeholder comment
  • Drop ReactorSkipWinAppSDKInjection and the explicit split-package references in tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj (they exist only because the experimental aggregate caps WinUI below 3.0.0 and cannot resolve alongside the WinUI slice)
  • Confirm CI is green, then take this out of draft

No other change should be required.

Related

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🧪 Merged coverage

Caution

Coverage run failed for 7b0195f (build or measurement error). See the workflow run for details.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📦 Build metrics

Caution

The build-metrics run did not produce artifact sizes (build failed). See the workflow run.

Copilot AI added 2 commits August 25, 2026 15:27
Reactor builds two DataTemplates at runtime by handing XamlReader.Load a
markup string: the ContentControl shell every items control mounts
(ListView/GridView/FlipView), and the legacy text-node TreeView template.
Both exist only because DataTemplate could not be constructed from code.

microsoft-ui-xaml adds `new DataTemplate(DataTemplateElementFactory)`, which
takes a callback returning the subtree. This ports both templates onto it.

Beyond removing a runtime XAML parse, the TreeView template loses its
`{Binding Content.Content}`. A classic Binding resolves that path by string
through CsWinRT's ICustomPropertyProvider — reflection, which NativeAOT trims
unless the source type is annotated (that annotation is what #1108 added), and
which costs a reflective lookup per realized row even when it works. Reading
the same chain in a DataContextChanged handler is strongly typed: no
annotation needed, no lookup, and the event re-fires on recycling so reused
rows retext correctly.

The `[WinRT.GeneratedBindableCustomProperty]` attribute on TreeViewNodeData is
deliberately left in place. Reactor no longer needs it, but it is public API
surface that consumers binding to TreeViewNodeData themselves still rely on.

DO NOT MERGE YET. The constructor is gated behind
`[feature(Feature_ExperimentalApi)]` in microsoft-ui-xaml and has not shipped
in a public Windows App SDK, so CI cannot restore this branch: the versions in
Directory.Build.props are an internal experimental build and do not resolve
from public feeds. When the API ships, update those two versions and drop the
metapackage opt-out in the selftest host; no other change should be needed.

Validated against Microsoft.WindowsAppSDK.WinUI 3.0.0-experimental.260818.21
(the PR-built native core, Microsoft.ui.xaml.dll 3.3.0.2608): full selftest
suite 1447 planned / 0 failures, with the text-node TreeView fixture green on
both the mount and update arms.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 17bce8bb-d57a-4f76-a55d-27c9029fc9a3
#1108 annotated TreeViewNodeData with
[WinRT.GeneratedBindableCustomProperty] for exactly one reason: Reactor's own
text-node TreeView template bound "Content.Content", and that string path is
resolved by reflection, which NativeAOT trims. This branch replaces that
binding with a DataContextChanged handler, so the reason is gone -- there is
now no {Binding} anywhere in src/ and no SetBinding call site.

The attribute never shipped: v0.1.0-preview.13 and every earlier tag predate
#1108, so no released build has it, and binding to TreeViewNodeData from a
consumer-supplied template was already broken under NativeAOT in all of them.
Removing it therefore takes nothing away that ever worked in a release; it
declines to keep a side effect of an internal fix. Keeping it would carry a
workaround, plus `partial` on a public record, for a scenario Reactor does not
use and does not document.

The limitation is now written down instead: docs/aot-support.md gains a row
explaining that a classic {Binding} in a hand-supplied DataTemplate is not
trim-safe, that Reactor's own templates avoid it by populating content in a
DataContextChanged handler, and that a consumer installing their own template
via an escape hatch should do the same or annotate their own type.

Also disambiguates the IElementFactory cref in IItemsRepeaterFactorySource.
The Windows App SDK this branch targets adds Microsoft.UI.Xaml.IElementFactory
alongside the existing Microsoft.UI.Xaml.Controls.IElementFactory, so the bare
cref became ambiguous (CS0419) and failed the warnings-as-errors Release
build. Points at the Controls one, which is what the member returns.

Validated: src/Reactor Release build 0 warnings / 0 errors; selftest suite
1447 planned with only the known WindowLevel_RuntimeFlip_Topmost Z-order flake
failing under full-suite load (passes 3/3 in isolation, unrelated to these
files).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 17bce8bb-d57a-4f76-a55d-27c9029fc9a3
@azchohfi
Alexandre Zollinger Chohfi (azchohfi) force-pushed the azchohfi-winui3-integration-datatemplate branch from 9cfd5c5 to 7b0195f Compare August 25, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants