Skip to content

Map the experimental WinUI TableView and Chart into Reactor.Advanced - #1357

Draft
Alexandre Zollinger Chohfi (azchohfi) wants to merge 14 commits into
mainfrom
azchohfi-winui-experimental-tableview-charts
Draft

Alexandre Zollinger Chohfi (azchohfi) wants to merge 14 commits into
mainfrom
azchohfi-winui-experimental-tableview-charts

Conversation

@azchohfi

@azchohfi Alexandre Zollinger Chohfi (azchohfi) commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Moves the repo to the Windows App SDK 2.5.4-experimental channel (WinUI 2.3.10-experimental), which ships two new control families, and maps both into Reactor.Advanced:

  • TableView (Microsoft.UI.Reactor.Advanced.Tabular, over Microsoft.UI.Xaml.Controls.Tabular.TableView), designed MVU-first:
    • TableView(items, columns...) takes immutable record snapshots. It diffs each new snapshot into a collection it owns (.KeyRows<T>(...) for identity), so the table never resets: selection, scroll and active edits survive state changes.
    • TextColumn<T>(header, read, onEdit) is the platform's own text column (accessible TextBlock cell, TextBox editor, UIA value pattern), fed by a pathless binding through a C# converter. No reflection, no INotifyPropertyChanged, NativeAOT-safe. Edits arrive as a callback (p with { Name = text }), and nothing is written into the row.
    • TemplateColumn<T>(header, row => Element): Reactor-rendered cells in a code-built DataTemplate.
    • BoundColumn(header, path) keeps classic {Binding} available for INPC models.
    • Also: typed SortBy, FilterRows/GroupRows (via TableViewSource), group-header/empty content, frozen columns, density, grid lines, selection.
    • Spec 058 hybrid: the 8 scalar control properties (selection mode, density, grid lines, headers, sort/resize toggles, row brushes) come from [GenerateReactorDescriptor] with ClearValueOnUnset. The generated static constructor registers the control. Rows, columns, templates, the effective IsReadOnly, controlled selection and the events are hand-written in the Customize hook. The events stay hand-written because a control holds a single event-payload box, so generated and custom events can't be mixed. Chart stays a hand-coded handler: the generator would cover only 2 of its properties.
  • Chart (Microsoft.UI.Reactor.Advanced.Charts, over Microsoft.UI.Xaml.Controls.Charts.Chart): Chart(LineSeries/BarSeries/AreaSeries...) with category / date-time / linear axes, legend, markers and data labels. Everything updates in place. Shared axes and Samples are reconciled according to the native rules found while building this:
    • A linear axis is invalid in the X slot.
    • Horizontal bars (the default orientation) can't share axes with other layouts.
    • Shared axes need shared Samples.
  • Samples: samples/TableView.Sample, samples/WinUIChart.Sample.

This supersedes #621, which hand-built a projection and checked in native binaries to reach a pre-release TableView. The control now ships in the SDK, so #621 can be closed.

Fixes found while getting CI green

  • Core: .OnUnmount could silently never fire.
    • Cause: the action was kept in a ConditionalWeakTable keyed by the control's managed wrapper. If a GC collects that wrapper between mount and unmount, the unmount walk sees a fresh wrapper over the same native control and misses the entry.
    • Fix: the action now lives on the native element's ReactorState, like the other per-element state that was already moved for this reason.
    • Test: new deterministic fixture LT_OnUnmountSurvivesWrapperCollection forces the GC. 0/10 unmounts fired before the fix, 10/10 after. This was the intermittent LT_OnMountUnmountBalanced failure.
  • Per-control state for TableView and Chart is stored in an attached DP on the native control. A ConditionalWeakTable keyed by the managed wrapper has the same wrapper-collection hazard as above.
  • NativeAOT: snapshot rows are bound through a named partial collection (TableRows). Without it, CsWinRT can't project the collection and every TableView mount failed under AOT.
  • PDM identity fixtures: they compared identity hashes of managed wrappers, which change when a wrapper is collected and re-created. They now compare by reference, like their sibling fixtures.
  • CI plumbing for the experimental channel:
    • The doc gate now exempts CS8305; its command-line WarningsNotAsErrors overrides the props file.
    • bootstrap pins the scaffold to the repo's prerelease SDK (the template's Version="*" never resolves a prerelease).

Linked issue / spec

Supersedes #621. Updates spec 063 §3.2 (the D1 tripwire fired; see below).

Test plan

  • Unit tests: tests/Reactor.Tests/ExperimentalControlsTests.cs cover value normalization, axis inference and identity, the snapshot diff (RowDiff), the text-column commit rule and the column helpers.
  • Selftest fixtures: TableViewFixtures (7) and ChartFixtures (3).
    • Mount, in-place update, structural column change.
    • Controlled selection with no echo (mutation-checked).
    • Filter/group/empty, unmount cleanup.
    • Immutable snapshots: bound collection stable, selection preserved.
    • A real edit committed through the platform's cell automation peer into onEdit.
    • Chart: series/axes/samples sharing, renderer attached, in-place updates, series type change.
  • dotnet test tests/Reactor.Tests: passed, 14,756 succeeded, 0 failed (after merging main).
  • Selftests for TableView, Chart, LT and PDM pass under both JIT and a local NativeAOT publish. The one exception is TableView_TextColumnEditViaAutomation, skipped under AOT with a documented reason: in-process, CreatePeerForElement returns no peer for a row.
  • Full selftest corpus locally: only the pre-existing AnchorReset/Issue487/ItemsView_Parking failures, which fail identically on unmodified main on this machine.
  • dotnet pack src/Reactor.Advanced: the nuspec gains no generator or abstractions dependency.
  • CI: all 44 checks green on the head commit, which includes the generator hybrid and the main merge. This includes Build solution, AOT / packaged / regular selftests, coverage, doc gate and bootstrap (packaged scaffold build and launch).
  • Both samples built and run. Screenshots verified the chart and the table.

Risk / breaking changes

  • Repo-wide experimental SDK. WindowsAppSDKVersion is 2.5.4-experimental, and CS8305 is demoted to a warning. Merge only if we're fine shipping on the experimental channel. Otherwise keep this as the integration branch until the APIs go stable.
  • Unpackaged apps must be self-contained to use these controls. Their themes load from ms-appx:///, which a framework-dependent unpackaged app can't resolve. Reactor throws an explanatory InvalidOperationException. Samples set WindowsAppSDKSelfContained=true.
  • Core behavior change (bug fix): .OnUnmount now reliably fires even when the control's wrapper was collected. Previously such callbacks were silently skipped.
  • Mechanical consequences of the bump:
    • Literal SDK pins move to the new floor.
    • The motion-constant pin is re-verified: the WinUI sources are byte-identical at the new tag.
    • The spec 063 D1 tripwire fired: LocalSettings now writes the machine-local hive. The guard is now hive-agnostic. D1/D2 are not revisited here.
  • No other core public API changes; all new API is in Reactor.Advanced.

The WinUI TableView (Microsoft.UI.Xaml.Controls.Tabular) and Chart
(Microsoft.UI.Xaml.Controls.Charts) controls ship only in
Microsoft.WindowsAppSDK.WinUI 2.3.10-experimental, which the
2.5.4-experimental metapackage pairs with (its nuspec names WinUI
2.3.10-experimental and pins Runtime [2.5.4-experimental]).

Consequences of the bump:
- CS8305 is demoted to a warning (WarningsNotAsErrors): the XAML compiler's
  generated XamlTypeInfo references the SDK's experimental metadata providers.
- Disambiguate the IElementFactory cref (the SDK adds
  Microsoft.UI.Xaml.IElementFactory), which otherwise fails Release with CS0419.
- Literal SDK pins in recipes/docs/CLI template/perf script follow the central
  floor, as WinAppSDKReferenceGuardTests requires.
- Motion constants re-verified: the four WinUI transition sources are
  byte-identical at winui3/release/2.5.4-experimental (blob SHAs unchanged;
  the tag diff touches 300 other files).
- The spec 063 D1 tripwire fired: at Foundation 2.3.13 LocalSettings writes
  the machine-local hive (measured). The guard is now hive-agnostic and the
  spec records the measurement; D1 itself is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7feb5bd2-d8b3-4135-8599-8bef352004c4
Adds first-class Reactor elements for the experimental platform controls:

TableView (Microsoft.UI.Reactor.Advanced.Tabular)
- TableView(items, columns...) MVU-style: pass immutable record snapshots; the
  handler diffs each snapshot into a collection it owns (.KeyRows for identity),
  so the table never resets and selection/scroll/edits survive.
- TextColumn<T>(header, read, onEdit): the native text column (accessible
  TextBlock cell, TextBox editor, UIA value pattern) fed by a pathless binding
  through a C# converter. No reflection or INPC; edits arrive as a callback and
  nothing is written into the row.
- TemplateColumn<T>(header, row => Element): Reactor-rendered cells hosted in a
  code-built DataTemplate, re-rendered on recycle and unmounted with the table.
- BoundColumn(header, path) for classic {Binding} models.
- Sorting (typed SortBy), FilterRows/GroupRows (TableViewSource), group header
  and empty content, frozen columns, density, grid lines, selection.

Chart (Microsoft.UI.Reactor.Advanced.Charts)
- Chart(LineSeries/BarSeries/AreaSeries...) with category, date-time and
  linear axes, legend, markers and data labels.
- Axes are deduplicated by record equality per slot and layout (the native
  chart rejects sharing across horizontal bars and other series), value
  collections are shared per instance as XAML does, and everything updates in
  place.

Both handlers merge TabularControlsResources / XamlChartsResources on first use
and explain the self-contained requirement for unpackaged apps instead of
surfacing an opaque COM error.

Includes selftest fixtures, unit tests, samples (TableView.Sample,
WinUIChart.Sample), and skill/README/AOT docs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7feb5bd2-d8b3-4135-8599-8bef352004c4
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build metrics

Artifact sizes for 14c8880 vs the base branch (5ba98bf).

Packages (compressed .nupkg)

Artifact base PR Δ
Microsoft.UI.Reactor.nupkg 1.90 MB 1.91 MB +4.17 KB (+0.21%) ⚠️
Microsoft.UI.Reactor.Advanced.nupkg 493.3 KB 544.3 KB +51.0 KB (+10.34%) ⚠️
Microsoft.UI.Reactor.Devtools.nupkg 287.8 KB 287.8 KB +0 B (0.00%) ≈

Assemblies in Microsoft.UI.Reactor

Artifact base PR Δ
Reactor.Analyzers.dll 410.5 KB 410.5 KB +0 B (0.00%) ≈
Reactor.dll 2.65 MB 2.65 MB +0 B (0.00%) ≈
Reactor.Localization.Generator.dll 16.0 KB 16.0 KB +0 B (0.00%) ≈
Reactor.Wrappers.Abstractions.dll 10.5 KB 10.5 KB +0 B (0.00%) ≈
Reactor.Wrappers.Generator.dll 99.5 KB 99.5 KB +0 B (0.00%) ≈

Assemblies in Microsoft.UI.Reactor.Advanced

Artifact base PR Δ
Reactor.Advanced.dll 1.00 MB 1.11 MB +114.0 KB (+11.12%) ⚠️

Assemblies in Microsoft.UI.Reactor.Devtools

Artifact base PR Δ
Microsoft.UI.Reactor.Devtools.dll 787.5 KB 787.5 KB +0 B (0.00%) ≈

✅ smaller / ⚠️ larger / ≈ within noise. Sizes come from a Release dotnet pack on the CI runner: packages are the compressed .nupkg download size, assemblies the uncompressed DLL inside it.
workflow run.

Comment thread samples/WinUIChart.Sample/Program.cs Fixed
Comment thread tests/Reactor.AppTests.Host/SelfTest/Fixtures/ChartFixtures.cs Fixed
Comment thread tests/Reactor.AppTests.Host/SelfTest/Fixtures/ChartFixtures.cs Fixed
Comment thread tests/Reactor.AppTests.Host/SelfTest/Fixtures/TableViewFixtures.cs Fixed
Comment thread tests/Reactor.AppTests.Host/SelfTest/Fixtures/TableViewFixtures.cs Fixed
Comment thread tests/Reactor.AppTests.Host/SelfTest/Fixtures/TableViewFixtures.cs Fixed
Comment thread tests/Reactor.AppTests.Host/SelfTest/Fixtures/TableViewFixtures.cs Fixed
Comment thread tests/Reactor.AppTests.Host/SelfTest/Fixtures/TableViewFixtures.cs Fixed
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

🧪 Merged coverage

Coverage for 14c8880 vs the base branch (5ba98bf) — unit + selftest merged.

Metric base PR Δ
Line 87.34% 87.24% -0.10 pp ⚠️
Branch 76.77% (978/1274) 75.69% (987/1304) -1.08 pp ⚠️

✅ higher / ⚠️ lower / ≈ within noise. Δ is in percentage points; coverage is unit + selftest merged (Debug x64) on the CI runner. Cobertura reports attached to the workflow run as artifacts.

- Bind snapshot rows through a named partial collection (TableRows). Under
  NativeAOT CsWinRT cannot project an anonymous ObservableCollection<object?>
  assigned to the object-typed ItemsSource ("items must implement a supported
  collection interface"), which failed every TableView mount in the AOT host.
- Split the edit-through-automation check into its own fixture and AOT-skip it:
  in-process under NativeAOT CreatePeerForElement returns no peer for a
  TableViewRow, so the fixture cannot reach the cell peer. Every other
  TableView fixture, including the immutable-snapshot one, runs under AOT.
  The fixture now uses the platform's own row/cell peers rather than a
  hand-constructed one.
- Doc snippet gate: a command-line WarningsNotAsErrors is a global property that
  replaces the props value, so it must exempt CS8305 too.
- Bootstrap: the reactor template floats Microsoft.WindowsAppSDK at "*", which
  never resolves a prerelease; when the repo pin is a prerelease, pin the
  scaffold to it (NU1107 otherwise).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7feb5bd2-d8b3-4135-8599-8bef352004c4
- The reactor template's own post-creation restore fails under a prerelease pin
  (its Version="*" float resolves stable), making `dotnet new` exit non-zero
  before the pin step ran. Tolerate that exit only when the repo pin is a
  prerelease and the project was produced; the explicit restore after pinning
  remains the real check.
- LT_OnMountUnmountBalanced failed intermittently in CI (0/25 locally) with no
  diagnostics; its checks now report the observed mount/unmount counts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7feb5bd2-d8b3-4135-8599-8bef352004c4
Get-PinnedWindowsAppSdkVersion reads with [IO.File], which resolves a relative
path against the process directory, not the pwsh location pushed into
TestProjects.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7feb5bd2-d8b3-4135-8599-8bef352004c4
The native control can release (and re-context) realized shells after the
table is gone; a late DataContextChanged must not re-enter the reconciler on
the unmounted table's behalf.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7feb5bd2-d8b3-4135-8599-8bef352004c4
Per-control .OnUnmount actions lived in a ConditionalWeakTable keyed by the
FrameworkElement wrapper. Nothing keeps a plain control's RCW alive, so after a
GC between mount and unmount the unmount walk sees a fresh wrapper over the same
native control, the lookup misses, and the callback silently never runs. Move
the action onto the native element's ReactorState, as was already done for the
echo counter, validation bindings and managed resource keys for the same reason.

This is what LT_OnMountUnmountBalanced hit intermittently in CI (exactly the last
batch's 10 unmounts missing); GC timing in shard 2 shifted with this branch's
added fixtures. LT_OnUnmountSurvivesWrapperCollection forces the GC and is
deterministic: 0/10 unmounts before the fix, 10/10 after.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7feb5bd2-d8b3-4135-8599-8bef352004c4
…tity hash

PDM_Stack/WrapGrid KeyedSwap compared RuntimeHelpers.GetHashCode of the Border's
managed wrapper. A GC between the two reads collects and re-projects the wrapper,
changing its identity hash with no change to the native tree, so the check failed
spuriously (twice in the AOT shard). Hold the original wrappers and compare by
reference, as the sibling fixtures in this file already do; a live wrapper keeps
CsWinRT returning that same instance for the native Border.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7feb5bd2-d8b3-4135-8599-8bef352004c4
Resolves the ReactorState conflict by keeping both new fields: this branch's
OnUnmountAction and main's ComponentDiagnosticId.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7feb5bd2-d8b3-4135-8599-8bef352004c4
- Compare layout doubles/floats within a tolerance instead of ==.
- Make the TableView unmount fixture's effect counters instance-owned, handed
  to cells through props, instead of static fields written per run.
- !table.IsReadOnly instead of == false.
- AddDays(7.0 * i) in the chart sample.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7feb5bd2-d8b3-4135-8599-8bef352004c4
…brid)

The 8 scalar TableView properties (SelectionMode, Density, GridLinesVisibility,
HeadersVisibility, CanUserSortColumns, CanUserResizeColumns, RowBackground,
AlternatingRowBackground) now come from [GenerateReactorDescriptor] with
ClearValueOnUnset; the generated static constructor registers the descriptor.
The rows, columns, templates, effective IsReadOnly, controlled selection and
all events stay hand-written in the Customize hook (TableViewSync).

Per-control state for TableView and Chart moves from a ConditionalWeakTable
keyed by the managed wrapper to an attached DependencyProperty on the native
control, so a fresh CsWinRT wrapper for the same control still finds it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7feb5bd2-d8b3-4135-8599-8bef352004c4
…mental-tableview-charts

# Conflicts:
#	src/Reactor/Core/Reconciler.cs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7feb5bd2-d8b3-4135-8599-8bef352004c4
…mental-tableview-charts

# Conflicts:
#	src/Reactor/Core/Reconciler.cs

This branch has not been deployed

No deployments
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.

1 participant