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:

ClassResponsibilityLines
GridGroupEngineOrchestrates all grouping operations, manages descriptors and group tree436
GridGroupTree node representing one group with its rows, sub-groups, and collapse state152
GroupDescriptorDefinition of a grouping level: column name, sort direction, display format, aggregates98
IGridGrouperInterface for custom grouping logic; DefaultGridGrouper is the built-in implementation84
GridGroupSummaryRowDisplays aggregate values (sum, avg, count, min, max) at group footer67
GridGroupHeaderRendererRenders group header with chevron, key display, item count, and expand/collapse93

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.

MemberTypeDescription
DescriptorsProperty: List<GroupDescriptor>Ordered list of group descriptors defining nesting hierarchy
GroupsProperty: List<GridGroup>Root-level groups after ApplyGrouping()
IsGroupedProperty: boolTrue when at least one descriptor is registered and grouping is applied
GrouperProperty: IGridGrouperInjectable grouper instance; defaults to DefaultGridGrouper

Methods

MethodSignatureDescription
AddDescriptorvoid AddDescriptor(GroupDescriptor descriptor)Appends a grouping level and re-applies grouping
ClearDescriptorsvoid ClearDescriptors()Removes all descriptors, ungroups rows, restores flat view
SetDescriptorsvoid SetDescriptors(IEnumerable<GroupDescriptor> descriptors)Replaces entire descriptor set in one operation
ToggleCollapsevoid ToggleCollapse(GridGroup group)Flips IsCollapsed on the given group node
ExpandAllvoid ExpandAll()Recursively sets IsCollapsed = false on all groups
CollapseAllvoid CollapseAll()Recursively sets IsCollapsed = true on all groups
ApplyGroupingvoid ApplyGrouping()Rebuilds the group tree from the current data source using IGridGrouper
SortWithinGroupsvoid SortWithinGroups(string columnName, SortDirection direction)Sorts rows inside each group by the given column/direction
SortGroupsByColumnvoid SortGroupsByColumn(string columnName, SortDirection direction)Reorders top-level groups themselves by the specified column
GetHeaderHeightint GetHeaderHeight(int level)Returns pixel height of group header for a given nesting level
GetHeaderCountBeforeRowint GetHeaderCountBeforeRow(int dataRowIndex)Count of group/summary headers before a flat row index (for scroll math)
GetTotalContentHeightint GetTotalContentHeight()Full scrollable height including all headers, rows, and summary rows
GetTotalHeightBeforeRowint 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.

PropertyTypeDefaultDescription
ColumnNamestring—Data column to group by (required)
SortDirectionSortDirectionAscendingSort order for group keys: Ascending or Descending
DisplayFormatstring"{0}"Format string for the group key value in the header (e.g., "Year: {0}")
ShowSummaryRowboolfalseWhen true, renders a GridGroupSummaryRow below each group
AggregatesList<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.

PropertyTypeDescription
KeyobjectThe grouped value (e.g., "USA" for a Country group)
ValuestringFormatted display string (applies DisplayFormat from descriptor)
LevelintNesting depth (0 = top level, 1 = first sub-group, etc.)
IsCollapsedboolTrue when group is collapsed; child rows and sub-groups are hidden
RowsArrayListData rows belonging to this group (leaf rows, not in sub-groups)
SubGroupsList<GridGroup>Child groups for the next descriptor level
ParentGridGroupReference to parent group; null for root-level groups
SummaryValuesDictionary<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().

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:

OperationMethodScope
Toggle one groupToggleCollapse(GridGroup)Single group — children hidden/shown
Expand allExpandAll()All groups at all levels
Collapse allCollapseAll()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]);
    }
}