Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

TreeDataGrid

Hand the grid a ChildrenSelector and rows nest: the first visible column carries the indent and the expand caret, and no extra column is added. TreeDataGrid is the same control under a name that says what it is — every other grid feature (columns, sorting, filtering, frozen columns, selection, editing, aggregates) works exactly as it does on a flat grid.

@* Blazor *@
<TreeDataGrid TItem="CostNode" Items="accounts"
ChildrenSelector="n => n.Lazy ? null : n.Children"
ChildrenLoader="LoadChildrenAsync"
HasChildrenSelector="n => n.Lazy || n.Children.Count > 0"
TreeIndentSize="18"
FilterMode="DataGridFilterMode.Toolbar"
Striped="true" Bordered="true">
<Columns>
<PropertyColumn Property="x => x.Name" Title="Account" />
<PropertyColumn Property="x => x.Kind" />
<PropertyColumn Property="x => x.Budget" Format="C0" />
</Columns>
</TreeDataGrid>
<!-- MAUI - the selectors are BindableProperties, so they bind straight to a view model -->
<shiny:TreeDataGrid ItemsSource="{Binding Accounts}"
ChildrenSelector="{Binding ChildrenSelector}"
ChildrenLoader="{Binding ChildrenLoader}"
HasChildrenSelector="{Binding HasChildrenSelector}"
TreeIndentSize="18"
Striped="True" Bordered="True">
<shiny:DataGridColumn Title="Account" PropertyName="Name" Width="2*" />
<shiny:DataGridColumn Title="Kind" PropertyName="Kind" Width="1*" />
<shiny:DataGridColumn Title="Budget" PropertyName="Budget" StringFormat="{}{0:C0}" Width="1.2*" />
</shiny:TreeDataGrid>

ChildrenSelector answers from memory. ChildrenLoader fetches a level the first time its row is expanded — a spinner replaces that row’s caret meanwhile — and the result is cached until you call InvalidateChildren. If the fetch throws, the row collapses again and ChildrenLoadFailed reports it.

Give HasChildrenSelector when you want leaves to render caret-free before anything has loaded. Without it a row with a loader attached and nothing cached is assumed to have children — better to offer a caret than to hide a branch that turns out to have something in it.

  • Sorting and filtering apply per level, so children stay under their parent instead of being scattered into a global order.
  • A row is kept when a descendant matches the filter, even if the row itself does not — otherwise the match would sit under a pruned parent and be unreachable. Branches that have not loaded yet are kept for the same reason: they cannot be searched, so they are not pruned on a guess.
  • Paging pages the roots, and footer aggregates are computed over the roots too.
  • Tree mode and Groupable are mutually exclusive — grouping wins, since the two impose competing row orders.
  • ExpandAll opens every already-loaded level. It does not fetch: the depth of a lazily loaded tree is unbounded.

A tree row can also carry a detail row — set both ChildrenSelector and RowDetailTemplate and the caret column drives the breakdown while the inline caret drives the children. Reach for a detail row when the extra content is about the row, and for tree mode when the extra rows are more of the same thing one level down.