Table of Contents

Grouping

TableView supports multi-level column grouping: collapsing rows into collapsible headers by one or more column values, with expand/collapse control per group and full integration with column sorting.

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.

When to use it

Use grouping when users need to see data organized into categories - for example, orders grouped by status, or employees grouped by department and then by role. Combine it with sorting to control the order of both the groups and the items inside them.

Basic example

Grouping is off by default. The simplest way to enable it is through the column header's options menu - open a column's options flyout and choose Group:

<tv:TableView ItemsSource="{x:Bind Products}" />

Clicking Group on the Category column collapses the rows into headers, one per distinct category value.

Grouping programmatically

Grouping descriptions live on TableView.GroupDescriptions, similar to SortDescriptions:

tableView.GroupDescriptions.Add(new GroupDescription("Category"));

Add a second GroupDescription to group by more than one level - each additional description nests inside the previous one:

tableView.GroupDescriptions.Add(new GroupDescription("Department"));
tableView.GroupDescriptions.Add(new GroupDescription("Role"));

This produces a two-level hierarchy: a header per department, and inside each, a header per role.

Remove all grouping:

tableView.UngroupAll();

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.

Ungrouping and later re-grouping the same column starts every group at DefaultGroupState again - any groups a user had individually expanded or collapsed before ungrouping do not carry over.

Group headers: name and count

By default, each group header shows the group's key and item count, e.g. Electronics (12). This comes from TableView.DefaultGroupStyle, which is a plain GroupStyle applied automatically unless you add your own entry to TableView.GroupStyle (a ListViewBase.GroupStyle collection):

<tv:TableView ItemsSource="{x:Bind Products}">
    <tv:TableView.GroupStyle>
        <GroupStyle>
            <GroupStyle.HeaderTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal" Spacing="8">
                        <FontIcon Glyph="&#xE8B7;" />
                        <TextBlock Text="{Binding Key}" FontWeight="Bold" />
                        <TextBlock Text="{Binding Count}" Opacity="0.6" />
                    </StackPanel>
                </DataTemplate>
            </GroupStyle.HeaderTemplate>
        </GroupStyle>
    </tv:TableView.GroupStyle>
</tv:TableView>

The header's DataContext is a TableViewGroupInfo with these members:

Member Type Description
Key object? The group's key value
Count int Total item count in the group, including nested subgroups
IsExpanded bool Whether the group's items are currently shown

Expanding and collapsing groups

Each group header has a built-in expand/collapse button. Clicking it toggles IsExpanded for that group and shows or hides its items (or, for a non-leaf group in a multi-level hierarchy, its descendant subgroups and items).

Default expanded/collapsed state

Control whether newly created groups start out expanded or collapsed with TableView.DefaultGroupState:

<tv:TableView DefaultGroupState="Collapsed" />
Value Description
Expanded Groups start expanded (all items visible)
Collapsed Groups start collapsed (only headers visible) - the default

Changing DefaultGroupState at runtime re-applies it to every group that has not been individually toggled away from the previous default. A group a user has explicitly expanded or collapsed keeps that state until toggled again.

Toggling a group programmatically

Each entry in TableView.CollectionView.CollectionGroups implements the standard ICollectionViewGroup interface; its Group property is the TableViewGroupInfo whose IsExpanded you can set directly:

var firstGroup = tableView.CollectionView.CollectionGroups?
    .Select(g => ((ICollectionViewGroup)g).Group)
    .OfType<TableViewGroupInfo>()
    .FirstOrDefault();

if (firstGroup is not null)
{
    firstGroup.IsExpanded = !firstGroup.IsExpanded;
}

Hiding the expand/collapse button

To let groups start collapsed/expanded via DefaultGroupState without exposing a user-facing toggle, hide the button with ShowGroupExpandCollapseButton:

<tv:TableView ShowGroupExpandCollapseButton="False" />

Groups can still be toggled programmatically while the button is hidden.

Sticky group headers

Enable sticky group headers so the current group's header stays pinned to the top of the viewport while scrolling through its items:

<tv:TableView AreStickyGroupHeadersEnabled="True" />

Note: This only pins a single level of header at a time - with multi-level grouping, ancestor headers above the innermost one do not stack.

Sorting and grouping together

Grouping a column and sorting it are unified: grouping a column also drives its order, so a grouped column shows a sort direction indicator like any other sorted column, and clicking it toggles that direction instead of adding a separate sort.

  • If the column already had its own sort applied when it's grouped, that sort description is kept (not discarded) and seeds the group's initial direction. Clicking the header keeps cycling through it three-state (ascending → descending → cleared) until it's eventually cleared - only then does ordering hand off fully to the group, which cycles two-state (ascending ↔ descending) from that point on. "Clear Sorting" is available up until that hand-off, as a shortcut past the three-state cycle.
  • Grouping a column with no prior sort starts the group directly in two-state mode with a default ascending direction - there's no independent sort description to cycle through first.
  • Either way, the chosen direction reorders both the group headers and the items inside each group.
  • Ungrouping a column (via the header's Ungroup option, or UngroupAll()) removes any sort description tied to it and clears its sort direction indicator.
// Groups by Category, then reorders both headers and each group's items descending
var categoryGroup = tableView.GroupDescriptions
    .OfType<GroupDescription>()
    .First();

categoryGroup.Direction = SortDirection.Descending;
tableView.RefreshGrouping();

Sorting groups by item count

By default, groups are ordered by their key value. Set GroupSortMode on a GroupDescription to order groups by how many items they contain instead - "show the biggest group first":

categoryGroup.SortMode = GroupSortMode.Count;
categoryGroup.Direction = SortDirection.Descending; // biggest group first
tableView.RefreshGrouping();

Direction still controls ascending/descending of whichever SortMode is set to. Groups with equal counts always break ties ascending by key, regardless of Direction. This is also available from a grouped column's own header flyout, as Sort Groups by Count / Sort Groups by Value.

Refreshing groups

Call TableView.RefreshGrouping() to rebuild groups from the current data without user interaction - useful after mutating data outside an ObservableCollection:

tableView.RefreshGrouping();

Row and header indentation

For multi-level grouping, row cells and column headers automatically shift right by 24px for each grouping level beyond the first, keeping the deepest group header's content aligned with the columns beneath it. The first grouping level adds no indent.

The Grouping event

Handle TableView.Grouping to intercept or supplement the built-in group logic when a column's Group option is used. Setting e.Handled = true prevents the default grouping from running:

tableView.Grouping += (s, e) =>
{
    if (e.Column.Header?.ToString() == "Id")
    {
        // Don't allow grouping by Id
        e.Handled = true;
    }
};

TableViewGroupingEventArgs properties:

Property Description
Column The column being grouped
Handled Set true to suppress default grouping behavior

Checking grouping state

bool isGrouped = tableView.IsGrouped; // true if any GroupDescription is applied

Common options

Property / Method / Event Description
CanGroupColumns Enables or disables grouping for all columns
CanGroup Per-column grouping toggle
GroupDescriptions Collection of active group descriptions
GroupSortMode Orders groups by key (default) or item count
DefaultGroupStyle The GroupStyle applied when no custom GroupStyle entry is set
DefaultGroupState Whether new groups start expanded or collapsed
ShowGroupExpandCollapseButton Shows or hides the per-group expand/collapse button
AreStickyGroupHeadersEnabled Pins the current group header to the top while scrolling
IsGrouped true if any grouping is applied
UngroupAll() Removes all grouping and resets affected columns' sort indicators
RefreshGrouping() Rebuilds groups from the current data without user interaction
Grouping Fires before the default group action runs; can be handled/suppressed

Notes and limitations

  • Grouping is Windows-only; it does not run on non-Windows Uno Platform targets.
  • A grouped column's order always needs a direction, so it never truly clears to unsorted the way an ungrouped column's third click does - see Sorting and grouping together for the three-state-until-cleared-then-two-state cycle.
  • Grouping and its implicit sort operate on the internal collection view. They do not mutate the original collection.
  • Sticky group headers pin only one header level at a time.