Repository navigation
Add multi-level column grouping support (Windows only) - #425
Conversation
Introduces Excel-like grouping to TableView for WinUI 3 (Windows only): - Adds GroupDescriptions, CollectionGroups, and group state APIs/events. - Implements TableViewGroupHeaderRow with expand/collapse and indentation. - Adds CanGroup to TableViewColumn and UI commands for grouping/ungrouping. - Updates CollectionView for grouped/filtered/sorted views and state changes. - Updates XAML styles, row alignment, and selection logic for grouping. - All grouping features are conditionally compiled for Windows only.
Expanded CollectionViewTests.cs with comprehensive UI tests for grouping behaviors, including single/multi-level grouping, group state transitions, persistence, and group ordering. Added TableViewGroupingSelectionTests.cs to test TableView selection and row/cell access in grouped and ungrouped scenarios, covering selection, group expand/collapse, ScrollRowIntoView, and cell retrieval after group changes. Includes helper methods for test data and group state manipulation.
Added detailed docs for new multi-level column grouping, including a dedicated grouping.md article. Updated README, overview, and feature comparison to highlight grouping (Windows-only) and linked to new docs. Clarified grouping's Windows-only status in Uno-related and performance docs. Expanded API/event reference for Grouping event and TableViewGroupingEventArgs. Updated migration guides to map grouping APIs and note differences from WPF/WCT. Added grouping notes to accessibility, AOT, and performance docs. Updated toc.yml to include grouping article.
There was a problem hiding this comment.
Pull request overview
Adds Excel-like multi-level grouping for TableView on Windows targets by extending the internal CollectionView with GroupDescriptions/CollectionGroups, rendering group header rows with expand/collapse + indentation, and integrating grouping with the existing sorting UI/behavior. This PR also adds a new sample page and broad documentation coverage for the new feature set.
Changes:
- Add Windows-only grouping pipeline in
CollectionView(group descriptions, flattened nested groups, persisted expand/collapse state) and wire it intoTableView+ column header commands. - Introduce group header row visuals/styles and row indentation to align grouped headers with columns/cells.
- Add tests, sample page, and documentation updates describing grouping APIs, behavior, and limitations.
Reviewed changes
Copilot reviewed 49 out of 49 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/TableViewGroupingSelectionTests.cs | New UI tests for selection/current-cell/scrolling behavior under grouping and collapse/expand. |
| tests/CollectionViewTests.cs | New UI tests validating single/multi-level grouping, ordering, state persistence, and transitions. |
| src/Themes/TableViewRowPresenter.xaml | Adds an indent placeholder and adjusts vertical gridline alignment to support grouped indentation. |
| src/Themes/TableViewHeaderRow.xaml | Adds an “Ungroup all” menu item to the header options flyout. |
| src/Themes/TableViewGroupHeaderRow.xaml | New default template + style resources for group header rows and default GroupStyle. |
| src/Themes/TableViewColumnHeader.xaml | Adds a “Group/Ungroup” menu option to the per-column options flyout. |
| src/Themes/TableView.xaml | Merges group header resources and sets a default DefaultGroupStyle in the default TableView style. |
| src/Themes/Resources.xaml | Adds theme resources for group header row look-and-feel (background/foreground/margins/font size). |
| src/Themes/Generic.xaml | Merges group header row resources and adds default style mapping for TableViewGroupHeaderRow. |
| src/TableViewRowPresenter.cs | Implements per-row cell indentation based on grouping depth. |
| src/TableViewHeaderRow.OptionComamnds.cs | Adds an “Ungroup all” command wiring for the header flyout. |
| src/TableViewHeaderRow.cs | Prevents select-all checkbox recursion by guarding programmatic IsChecked updates. |
| src/TableViewGroupState.cs | New enum defining default group expanded/collapsed state. |
| src/TableViewGroupHeaderRow.cs | New group header container with expand/collapse toggle and gridline integration (Windows-only). |
| src/TableViewColumnHeader.OptionComamnds.cs | Wires up per-column “Group/Ungroup” command and adjusts “Clear sorting” availability for grouped columns. |
| src/TableViewColumnHeader.cs | Implements group/ungroup behavior, grouping-driven sort toggling, and two-state vs three-state sort cycling when grouped. |
| src/TableView.Properties.cs | Adds new grouping-related dependency properties and APIs (Windows-only). |
| src/TableView.Events.cs | Adds TableView.Grouping event (Windows-only). |
| src/TableView.cs | Hooks grouping-related CollectionView events, adds UngroupAll(), updates navigation math to use visible Items.Count, and refreshes row indent on grouping resets. |
| src/ItemsSource/TableViewGroupInfo.cs | New bindable DependencyObject carrying group key/count/level/expanded state and notifying CollectionView on expand/collapse. |
| src/ItemsSource/SortDescription.cs | Makes Direction settable and adds a default direction value. |
| src/ItemsSource/GroupDescription.cs | New type deriving from SortDescription to represent grouping operations. |
| src/ItemsSource/ColumnSortDescription.cs | Adds default sort direction parameter. |
| src/ItemsSource/ColumnGroupDescription.cs | New group description tied to a TableViewColumn (bound property vs rendered content behavior). |
| src/ItemsSource/CollectionViewGroup.cs | New ICollectionViewGroup implementation for flattened group headers (Windows-only). |
| src/ItemsSource/CollectionView.Properties.cs | Adds GroupDescriptions, DefaultGroupState, and a settable CollectionGroups (Windows-only). |
| src/ItemsSource/CollectionView.Events.cs | Adds PropertyChanging event support for grouping-related rebinding scenarios. |
| src/ItemsSource/CollectionView.cs | Implements grouping build/rebuild logic, flattened nested groups, and expanded-state persistence (Windows-only). |
| src/EventArgs/TableViewGroupingEventArgs.cs | New handleable event args for the grouping event. |
| src/Columns/TableViewColumn.cs | Adds CanGroup DP to allow per-column grouping enablement. |
| src/Collections/ObservableVector.cs | New IObservableVector<T> implementation used by grouping and group item vectors. |
| samples/WinUI.TableView.SampleApp/Pages/GroupingPage.xaml.cs | New sample page code-behind to exercise grouping with add/insert/move/delete operations. |
| samples/WinUI.TableView.SampleApp/Pages/GroupingPage.xaml | New sample page UI showcasing grouping and grouping-related options. |
| samples/WinUI.TableView.SampleApp/MainWindow.xaml.cs | Adds navigation routing entry for the new Grouping sample page. |
| samples/WinUI.TableView.SampleApp/MainWindow.xaml | Adds “Grouping” nav item to the sample app UI. |
| README.md | Adds grouping to the feature list and documentation links. |
| docs/index.md | Adds grouping to the docs landing page feature matrix. |
| docs/docs/toc.yml | Adds grouping to the docs TOC. |
| docs/docs/sorting.md | Documents sorting behavior changes when a column is grouped. |
| docs/docs/performance.md | Documents grouping performance characteristics and tradeoffs. |
| docs/docs/overview.md | Adds high-level overview section for grouping and platform availability. |
| docs/docs/migration-wpf.md | Updates migration guide with new grouping-related APIs/events. |
| docs/docs/migration-wct.md | Updates migration guide with new grouping-related APIs/events and guidance. |
| docs/docs/grouping.md | New primary documentation page for grouping usage and APIs. |
| docs/docs/getting-started-with-uno.md | Documents grouping as Windows-only for Uno targets. |
| docs/docs/datagrid-feature-comparison.md | Updates feature comparison to include grouping support details/limitations. |
| docs/docs/commands-events.md | Adds grouping event/type docs and TableViewGroupState reference. |
| docs/docs/aot-compatibility.md | Notes grouping’s reflection path implications for AOT (similar to sorting). |
| docs/docs/accessibility.md | Adds notes about group header accessibility/automation behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
Programmatic grouping is inaccessible externally, live regrouping misses property changes, and group-header accessibility and command-state issues remain.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (10)
Previously missed (8) — in code that hasn't changed since the last review.
src/ItemsSource/CollectionView.cs:338
- Live shaping only rebuilds groups when the changed property is also in
SortDescriptions. A column grouped without a prior independent sort has no sort description, so editing its grouping property leaves the item under the old group and leaves counts stale. Check relevantGroupDescriptionswhen no matching sort description handled the change.
if (GroupDescriptions.Count > 0)
{
HandleGroupChanged();
return;
}
src/TableViewGroupHeaderRow.cs:96
- Opacity and
IsHitTestVisibleonly hide this from pointer users; the invisible button remains in keyboard tab order and Space/Enter can still collapse the group. Disable keyboard focus and invocation as well while preserving the layout slot.
src/Themes/TableViewGroupHeaderRow.xaml:68 - This glyph-only
Buttonhas no accessible action name and does not expose the current expanded/collapsed state, so Narrator users cannot identify what it does or the group's state. Use an appropriately bound toggle/expand-collapse pattern and localized automation text.
src/TableViewColumnHeader.OptionComamnds.cs:54 - Once a column is grouped, setting either grouping permission to
falsedisables its Ungroup command, andGroup()also rejects the call before reaching the ungroup branch. Permissions should prevent adding a group, not prevent removing an existing one; allow execution whenIsGroupedand move theCanGroupguard after ungrouping.
src/TableView.cs:34 - Correct the typo in this field name and all references.
docs/docs/grouping.md:5 - On non-Windows targets
CanGroupColumnsis compiled out, so using it does not produce a no-op; it fails to compile. State that the property is unavailable and that only the header option is omitted.
This issue also appears on line 47 of the same file.
> **Note:** Grouping is currently a **Windows-only** feature. It is not available when running on non-Windows Uno Platform targets (macOS, Linux, WASM, iOS, Android). `CanGroupColumns` and the "Group" column header option are hidden/no-ops on those targets.
docs/docs/getting-started-with-uno.md:161
CanGroupColumnsis absent from non-Windows builds rather than being a property that has no effect, so the current guidance would lead Uno callers to a compile error.
- **Grouping is not available.** [Column grouping](grouping.md) is a Windows-only feature; `CanGroupColumns` and the column header's "Group" option have no effect on non-Windows Uno targets.
README.md:170
- Remove the unnecessary article in this sentence.
- **Sorting, Grouping, and Filtering**: Enable sorting, grouping, and filtering on specific columns or for the all columns.
src/TableViewColumnHeader.cs:129
- This ungroup path returns before raising
Grouping, while the new commands/events and migration documentation says that event fires before both group and ungroup actions. Either invoke the handleable event before this branch or document that ungrouping is not interceptable.
docs/docs/grouping.md:47 - Clearing
GroupDescriptionsdirectly does not have the same effect asUngroupAll(): it leaves a header-created column's companionSortDescriptionandSortDirectionintact. Document that distinction so callers do not expect the sort indicator and ordering to be reset.
Or clear the `GroupDescriptions` collection directly - both have the same effect, and both reset the sort indicator on any column that was driving a group.
- Files reviewed: 61/61 changed files
- Comments generated: 2
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Sorting-state duplication, stale live grouping, documentation discrepancies, and group-header accessibility issues remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (7)
Previously missed (7) — in code that hasn't changed since the last review.
src/TableViewGroupHeaderRow.cs:58
- Indentation is only assigned in
OnContentChanged, but a container's content can be prepared before its template, when_indentPlaceholderis still null.OnApplyTemplaterestores the visual state but not the indent, leaving nested headers aligned at level zero. ApplygroupInfo.Level * GroupIndentSizehere after resolving the template child.
src/TableViewGroupHeaderRow.cs:96 - Opacity and hit testing do not remove a button from keyboard navigation, so setting
ShowGroupExpandCollapseButton="False"leaves an invisible focusable control that can still be activated with Space/Enter. Disable keyboard focus and the control while hidden, preserving its layout slot as intended.
docs/docs/grouping.md:44 - Clearing
GroupDescriptionsdirectly does not have the same effect asUngroupAll(): it does not clear grouped columns'SortDirectionand can leave the associatedColumnGroupDescriptioninSortDescriptions. Document that callers should useUngroupAll()when they also want the sort state reset.
Or clear the `GroupDescriptions` collection directly - both have the same effect, and both reset the sort indicator on any column that was driving a group.
src/ItemsSource/CollectionView.cs:605
- Each item restored after a filter change calls
HandleItemAdded, which performs a fullHandleGroupChanged()rebuild when grouping is active. Removing a restrictive filter can therefore regroup the entire growing collection once per restored item (quadratic work and repeated UI resets). Batch these mutations and rebuild groups once after the loop.
if (HandleItemAdded(i, item, sourceIndex))
{
sourceIndex++;
src/Themes/TableViewGroupHeaderRow.xaml:68
- This icon-only button has no accessible name, so assistive-technology users cannot determine its purpose. Provide a localized automation name (and ideally expose its expanded/collapsed state through an appropriate automation pattern).
docs/docs/grouping.md:5 CanGroupColumnsis compiled out under#if WINDOWS; it is not a hidden/no-op property on non-Windows targets, and shared code that references it will fail to compile. State that the API is unavailable there (while the per-columnCanGroupproperty remains present but has no grouping effect).
> **Note:** Grouping is currently a **Windows-only** feature. It is not available when running on non-Windows Uno Platform targets (macOS, Linux, WASM, iOS, Android). `CanGroupColumns` and the "Group" column header option are hidden/no-ops on those targets.
docs/docs/getting-started-with-uno.md:161
CanGroupColumnsis not present on non-Windows builds because its declaration is guarded by#if WINDOWS, so describing it as having “no effect” incorrectly suggests cross-target code can still access it. Clarify that this API is unavailable and must be conditionally referenced.
- **Grouping is not available.** [Column grouping](grouping.md) is a Windows-only feature; `CanGroupColumns` and the column header's "Group" option have no effect on non-Windows Uno targets.
- Files reviewed: 63/63 changed files
- Comments generated: 3
- Review effort level: Balanced
…vergence OnCurrentCellChanged runs inside CurrentCellSlot's property-changed callback, synchronously within that SetValue. Driving ScrollIntoView and layout re-entrantly from there can fail natively, where .NET cannot catch it. Yield first so the SetValue unwinds (lifted from upstream be2aa5d). Placed after the generation is captured, it also collapses a held-arrow-key storm earlier than the existing guard did: every queued navigation yields here and all but the latest bail before touching the scroll machinery. Also documents the fork's divergence from upstream: upstream's grouping (PR w-ahmad#425) is the CollectionView's grouping and cannot run on the direct-binding path, so it is excluded permanently and ours stays. No further full merges; fixes by cherry-pick, features by reimplementation. The standing rejects and the procedure are written down.
Description
Adds Excel-like multi-level column grouping to
TableViewon Windows targets. Rows can be grouped by one or more column values via each column header's options flyout (or programmatically viaGroupDescriptions), with per-group expand/collapse, group headers showing key + item count, indentation for nested levels, and full integration with existing column sorting.Highlights:
GroupDescriptions/CollectionGroupsonCollectionView, withColumnGroupDescriptiondriving header-based groupingTableViewGroupHeaderRowrenders group headers with expand/collapse and per-level indentationTableViewAPIs:CanGroupColumns,DefaultGroupState,DefaultGroupStyle,ShowGroupExpandCollapseButton,AreStickyGroupHeadersEnabled,IsGrouped,UngroupAll(), and aGroupingeventCanGroup, plus a "Group"/"Ungroup" option in each column header's options flyoutCollectionViewgrouping behavior (single/multi-level, state transitions and persistence, ordering) andTableViewselection / row/cell access in grouped scenariosdocs/docs/grouping.mdarticle, plus updates across README, overview, sorting, performance, accessibility, AOT-compatibility, migration guides (WCT/WPF), feature comparison, and the Uno-platform doc to reflect the new featureRelated Issue
Closes #69
Type of Change
Checklist
mainbranchScreenshots / Recordings
Recording.2026-08-20.183028.mp4
Additional Notes
Grouping is Windows-only for now. Non-Windows Uno Platform targets compile out the grouping code paths entirely rather than exposing a degraded/no-op experience in the UI beyond the flyout option being hidden.