Skip to content

Host TableView rows in an ItemsRepeater with a custom VirtualizingLayout - #423

Closed
w-ahmad wants to merge 1 commit into
mainfrom
claude/tableview-itemsrepeater-migration-45d23b
Closed

w-ahmad wants to merge 1 commit into
mainfrom
claude/tableview-itemsrepeater-migration-45d23b

Conversation

@w-ahmad

@w-ahmad w-ahmad commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Summary

TableView used to derive from ListView, which is where its row virtualization, container generation, row selection storage, and the Windows row visual all came from. That base class had become the limiting factor:

  • Hit testing during drag selection scanned every item on every pointer-move frame and every 16 ms auto-scroll tick.
  • Row positions were maintained with a TransformToVisual walk per row, per scroll frame.
  • Cell selection materialised one entry per selected cell (SelectAll on a large grid allocated millions of structs).
  • ScrollRowIntoView was a 10-try realization poll with a magic + 8 correction to work around ListView's async realization.
  • Grouping was structurally impossible: the whole control assumed visual row == item index.

TableView now derives from Control and hosts its rows in an ItemsRepeater driven by a purpose-built VirtualizingLayout (TableViewRowsLayout). Realized row elements track the viewport height, not the item count — 200 items and 100,000 items in the same viewport realize the same number of rows. Row selection is a range-based model (TableViewSelectionModel), so selecting a million rows is one range and SelectedItems.Count is O(1). Hit testing, scroll-into-view, and PageUp/PageDown are index arithmetic against the layout's row-height store instead of scanning realized containers.

What's included

  • TableView : Control (was : ListView), TableViewRow : ContentControl (was : ListViewItem), with TableViewRow now owning its own visual-state machine (CommonStates, DisabledStates, MultiSelectStates, pointer-cancel/capture-lost handling).
  • The row template is unified across Windows and Uno (Windows previously used the native ListViewItemPresenter).
  • New src/Layout/ (TableViewRowsLayout, TableViewRowHeights, TableViewRowElementFactory) and src/Selection/ (TableViewSelectionModel, TableViewSelectedItemsCollection, TableViewCellSlotCollection).
  • Automation peers rewritten on FrameworkElementAutomationPeer with hand-written Selection/Scroll/ItemContainer providers. IGridProvider.GetItem now realizes off-screen rows on demand instead of returning null — fixes a real accessibility gap.
  • Shift+click range selection, previously delegated to ListViewItem, is implemented directly.
  • New sample page Virtualization: item-count switch (1k → 1M), select-all timing, jump-to-row, live realized-row counter that demonstrates the constant-row-count property.
  • Docs rewritten for the new architecture (overview.md, performance.md, accessibility.md, selection.md, incremental-loading.md, migration-wct.md, README, docs/index.md) plus a new docs/docs/breaking-changes.md.

Fixed along the way

  • An empty incrementally-loading source was not always asked for its first page.
  • A row recycled mid-edit silently dropped the edit and left IsEditing stuck true with the corner menu permanently disabled; it now commits the edit before the row is released.
  • A lost pointer capture (alt-tab, cancelled touch) left the table stuck in drag selection.
  • IGridProvider.GetItem returning null for off-screen rows, making large tables effectively unreadable to assistive technology.

Breaking changes

This is a deliberate semver-major change — see docs/docs/breaking-changes.md for the full table. Highlights:

  • CanDragItems/CanReorderItems row drag-reorder is not currently supported. It came entirely from ListViewBase with no code in this library, so it disappeared with the base class rather than being ported. ReorderRowsPage documents the gap; reimplementing it is tracked as follow-up work.
  • ItemTemplate, ItemsPanel, ItemContainerStyle, Header/Footer, IsItemClickEnabled, and a handful of other inherited ListViewBase/ItemsControl members are gone. Everything this repo's own code, samples, and tests actually use was re-declared on TableView with the same name/type.

Test plan

  • msbuild /restore /t:Build,Pack src/WinUI.TableView.csproj — green on all 6 target frameworks (net8/9/10, plain and -windows10.0.19041.0)
  • WinUI.TableView.Tests — 361/361 pass
  • WinUI sample app builds (x64)
  • Uno sample builds for net10.0-desktop

Stacked PR

Multi-level grouping is a separate follow-up PR stacked on this one.

🤖 Generated with Claude Code

TableView used to derive from ListView, which is where its row virtualization,
container generation, row selection storage, and the Windows row visual all
came from. That base class had become the limit: hit testing during drag
selection scanned every item on every pointer-move frame, row positions were
maintained with a visual-tree transform walk per row per scroll frame, cell
selection materialised one entry per selected cell, and scroll-into-view was
a 10-try realization poll with a magic +8 correction.

TableView now derives from Control and hosts its rows in an ItemsRepeater
driven by a purpose-built VirtualizingLayout (TableViewRowsLayout). Realized
row elements track the viewport height rather than the item count: 200 items
and 100,000 items in the same viewport realize the same number of rows.
Row selection is a range-based model (TableViewSelectionModel) so selecting
a million rows is one range and SelectedItems.Count is O(1). Hit testing,
scroll-into-view and PageUp/PageDown are index arithmetic against the
layout's row-height store instead of scanning realized containers.

Also included, since they follow directly from the base-class change:
- TableViewRow is now a ContentControl with its own visual-state machine
  (the row template is unified across Windows and Uno instead of Windows
  using the native ListViewItemPresenter).
- Automation peers are rewritten on FrameworkElementAutomationPeer with
  hand-written Selection/Scroll/ItemContainer providers; IGridProvider.GetItem
  now realizes off-screen rows on demand instead of returning null.
- Shift+click range selection, previously delegated to ListViewItem, is
  implemented directly.
- A recycled row with an in-flight cell edit now commits it instead of
  silently dropping it and leaving IsEditing stuck true.
- Row drag-reorder (CanDragItems/CanReorderItems), which came entirely from
  ListViewBase with no code in this library, is not currently supported.
  ReorderRowsPage documents the gap; see docs/docs/breaking-changes.md.
- Fixed: an empty incrementally-loading source was not always asked for its
  first page.

New sample page: Virtualization (item-count switch, select-all timing,
jump-to-row, live realized-row counter demonstrating the constant-row-count
property). Docs rewritten to describe the new architecture and the breaking
changes (README, overview, performance, accessibility, selection,
incremental-loading, migration-wct, and a new breaking-changes.md).

Verified: Build+Pack green on all 6 target frameworks (net8/9/10, plain and
-windows10.0.19041.0), 361/361 tests pass, WinUI sample builds x64, Uno
sample builds net10.0-desktop.

Grouping support is a separate follow-up PR stacked on this one.
@w-ahmad w-ahmad mentioned this pull request Aug 18, 2026
4 tasks done
@w-ahmad w-ahmad closed this Aug 19, 2026
@w-ahmad
w-ahmad deleted the claude/tableview-itemsrepeater-migration-45d23b branch September 24, 2026 16:02
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