GridX Grouping Engine
Multi-level grouping with aggregate summary rows, collapsible groups, nested GroupDescriptor, IGridGrouper, GridGroup model, group header rendering, and sort-within-groups
Table of Contents
Overview
The GridX grouping engine provides multi-level nested grouping on any column in the data grid. Groups are rendered as collapsible header rows with summary aggregate rows (sum, avg, count, min, max) below each group. The engine supports up to N-levels of nested grouping — each new GroupDescriptor added creates a sub-group within the previous level.
Key classes:
| Class | Responsibility | Lines |
|---|---|---|
GridGroupEngine | Orchestrates all grouping operations, manages descriptors and group tree | 436 |
GridGroup | Tree node representing one group with its rows, sub-groups, and collapse state | 152 |
GroupDescriptor | Definition of a grouping level: column name, sort direction, display format, aggregates | 98 |
IGridGrouper | Interface for custom grouping logic; DefaultGridGrouper is the built-in implementation | 84 |
GridGroupSummaryRow | Displays aggregate values (sum, avg, count, min, max) at group footer | 67 |
GridGroupHeaderRenderer | Renders group header with chevron, key display, item count, and expand/collapse | 93 |
GridGroupEngine (436 lines)
GridGroupEngine is the central coordinator attached to every BeepGridPro instance via the GroupEngine property. It owns the descriptor collection, the group tree, and delegates grouping logic to the injected IGridGrouper.
| Member | Type | Description |
|---|---|---|
Descriptors | Property: List<GroupDescriptor> | Ordered list of group descriptors defining nesting hierarchy |
Groups | Property: List<GridGroup> | Root-level groups after ApplyGrouping() |
IsGrouped | Property: bool | True when at least one descriptor is registered and grouping is applied |
Grouper | Property: IGridGrouper | Injectable grouper instance; defaults to DefaultGridGrouper |
Methods
| Method | Signature | Description |
|---|---|---|
AddDescriptor | void AddDescriptor(GroupDescriptor descriptor) | Appends a grouping level and re-applies grouping |
ClearDescriptors | void ClearDescriptors() | Removes all descriptors, ungroups rows, restores flat view |
SetDescriptors | void SetDescriptors(IEnumerable<GroupDescriptor> descriptors) | Replaces entire descriptor set in one operation |
ToggleCollapse | void ToggleCollapse(GridGroup group) | Flips IsCollapsed on the given group node |
ExpandAll | void ExpandAll() | Recursively sets IsCollapsed = false on all groups |
CollapseAll | void CollapseAll() | Recursively sets IsCollapsed = true on all groups |
ApplyGrouping | void ApplyGrouping() | Rebuilds the group tree from the current data source using IGridGrouper |
SortWithinGroups | void SortWithinGroups(string columnName, SortDirection direction) | Sorts rows inside each group by the given column/direction |
SortGroupsByColumn | void SortGroupsByColumn(string columnName, SortDirection direction) | Reorders top-level groups themselves by the specified column |
GetHeaderHeight | int GetHeaderHeight(int level) | Returns pixel height of group header for a given nesting level |
GetHeaderCountBeforeRow | int GetHeaderCountBeforeRow(int dataRowIndex) | Count of group/summary headers before a flat row index (for scroll math) |
GetTotalContentHeight | int GetTotalContentHeight() | Full scrollable height including all headers, rows, and summary rows |
GetTotalHeightBeforeRow | int GetTotalHeightBeforeRow(int flatIndex) | Cumulative pixel height before a given flat row index |
GroupDescriptor
GroupDescriptor defines one level of grouping. Multiple descriptors create a nested group hierarchy — e.g., {Country, City} produces Country groups with City sub-groups inside each.
| Property | Type | Default | Description |
|---|---|---|---|
ColumnName | string | — | Data column to group by (required) |
SortDirection | SortDirection | Ascending | Sort order for group keys: Ascending or Descending |
DisplayFormat | string | "{0}" | Format string for the group key value in the header (e.g., "Year: {0}") |
ShowSummaryRow | bool | false | When true, renders a GridGroupSummaryRow below each group |
Aggregates | List<GroupAggregate> | new() | Aggregate definitions for the summary row (sum, avg, count, min, max) |
GroupAggregate has properties: Function (enum: Sum, Average, Count, Min, Max), ColumnName (target numeric column), and DisplayFormat (e.g., "SUM: {0:N2}").
IGridGrouper
The grouping algorithm is abstracted behind IGridGrouper, enabling custom grouping logic injection. The built-in DefaultGridGrouper uses hash-based partitioning with stable sort order.
public interface IGridGrouper
{
List<GridGroup> BuildGroups(
IEnumerable dataSource,
List<GroupDescriptor> descriptors,
int level);
List<GridGroup> BuildSubGroups(
GridGroup parentGroup,
List<GroupDescriptor> descriptors,
int level);
}DefaultGridGrouper uses PropertyDescriptor to access column values, groups by ColumnName using a Dictionary<object, List> partition, sorts groups by SortDirection, and recursively calls BuildSubGroups for each subsequent descriptor level. To inject a custom grouper:
grid.GroupEngine.Grouper = new MyCustomGrouper();GridGroup Model
GridGroup is a tree node representing one group at any nesting level. Each group holds its child rows and optional sub-groups.
| Property | Type | Description |
|---|---|---|
Key | object | The grouped value (e.g., "USA" for a Country group) |
Value | string | Formatted display string (applies DisplayFormat from descriptor) |
Level | int | Nesting depth (0 = top level, 1 = first sub-group, etc.) |
IsCollapsed | bool | True when group is collapsed; child rows and sub-groups are hidden |
Rows | ArrayList | Data rows belonging to this group (leaf rows, not in sub-groups) |
SubGroups | List<GridGroup> | Child groups for the next descriptor level |
Parent | GridGroup | Reference to parent group; null for root-level groups |
SummaryValues | Dictionary<string, object> | Computed aggregate values when ShowSummaryRow is enabled |
The group tree is built by a recursive walk: root-level Groups contain SubGroups which contain further SubGroups, with leaf Rows at the deepest level. A flattened view for rendering is computed by GetHeaderCountBeforeRow() and GetTotalHeightBeforeRow().
Group Header Rendering
GridGroupHeaderRenderer draws each group's visual header row. It renders:
- Chevron icon — Expandable/collapsible indicator (▶ collapsed, ▼ expanded) drawn at left margin with level-based indentation
- Group key + display value — Column name prefix + formatted value, e.g.,
"Country: USA" - Item count — Number of leaf rows in this group, displayed as
"(247 items)" - Summary row — When
ShowSummaryRow = true, an additional row below the group rows displays aggregate values (Sum, Avg, Count, Min, Max) per theAggregateslist
Header height varies by level: GetHeaderHeight(int level) returns the pixel height, defaulting to 32px for level 0 and 28px for deeper levels. The renderer respects theme tokens via GridThemeHelper for colors and fonts.
Sort-Within-Groups
Rows inside each group can be independently sorted without affecting group ordering. SortWithinGroups(columnName, direction) iterates all groups and sorts their Rows by the given column. SortGroupsByColumn(columnName, direction) reorders the groups themselves at the top level.
var engine = grid.GroupEngine;
engine.AddDescriptor(new GroupDescriptor("Country", SortDirection.Ascending));
engine.AddDescriptor(new GroupDescriptor("City", SortDirection.Ascending));
engine.SortWithinGroups("Population", SortDirection.Descending);
engine.SortGroupsByColumn("Country", SortDirection.Descending);Sort direction uses the standard SortDirection enum (Ascending / Descending). When no sort is specified within groups, rows retain their original order from the data source.
Expand/Collapse
Each group can be independently collapsed or expanded. The engine provides both per-group and bulk operations:
| Operation | Method | Scope |
|---|---|---|
| Toggle one group | ToggleCollapse(GridGroup) | Single group — children hidden/shown |
| Expand all | ExpandAll() | All groups at all levels |
| Collapse all | CollapseAll() | All groups at all levels |
When a parent group is collapsed, all its child rows and sub-groups are hidden from rendering. The GetTotalContentHeight() method accounts for collapse state — collapsed groups contribute only their header height to the scroll height. The GetHeaderCountBeforeRow() method skips collapsed sub-groups when computing flat-to-visual row mapping.
Examples
Two-Level Grouping with Aggregates
var engine = grid.GroupEngine;
engine.AddDescriptor(new GroupDescriptor
{
ColumnName = "Category",
SortDirection = SortDirection.Ascending,
DisplayFormat = "Category: {0}",
ShowSummaryRow = true,
Aggregates =
{
new GroupAggregate { Function = AggregateFunction.Sum, ColumnName = "Sales", DisplayFormat = "Total: {0:C}" },
new GroupAggregate { Function = AggregateFunction.Average, ColumnName = "Sales", DisplayFormat = "Avg: {0:C}" },
new GroupAggregate { Function = AggregateFunction.Count, ColumnName = "ProductId", DisplayFormat = "Count: {0}" }
}
});
engine.AddDescriptor(new GroupDescriptor
{
ColumnName = "Region",
SortDirection = SortDirection.Ascending,
DisplayFormat = "Region: {0}",
ShowSummaryRow = true,
Aggregates =
{
new GroupAggregate { Function = AggregateFunction.Sum, ColumnName = "Sales", DisplayFormat = "Subtotal: {0:C}" }
}
});
engine.ApplyGrouping();Collapse All, Then Toggle One
grid.GroupEngine.CollapseAll();
var rootGroups = grid.GroupEngine.Groups;
if (rootGroups.Count > 0)
{
grid.GroupEngine.ToggleCollapse(rootGroups[0]);
if (rootGroups[0].SubGroups.Count > 0)
{
grid.GroupEngine.ToggleCollapse(rootGroups[0].SubGroups[0]);
}
}