Blazorise DataGrid: Self Reference

Self referencing allows a DataGrid row to render child rows. Use a self reference with @ref to control expansion programmatically.

Self Reference

This example uses ExpandRowTrigger and ReadChildData for hierarchical rows, and calls ExpandRow, CollapseRow, ExpandAllRows, and CollapseAllRows through a DataGrid self-reference.
Employee
City
Salary
Active
Samuel CollierNew Lura$86,030.41
Irvin ZiemannModestomouth$61,781.31
Gerald PollichTheresashire$58,810.75
1 - 3 of 3 items
3 items
<Button Color="Color.Primary" Margin="Margin.Is2.FromEnd" Disabled="@(!CanToggleSelectedRow)" Clicked="@ExpandSelectedRow">Expand Selected</Button>
<Button Color="Color.Secondary" Margin="Margin.Is2.FromEnd" Disabled="@(!CanToggleSelectedRow)" Clicked="@CollapseSelectedRow">Collapse Selected</Button>
<Button Color="Color.Success" Margin="Margin.Is2.FromEnd" Clicked="@ExpandAllRows">Expand All</Button>
<Button Color="Color.Warning" Clicked="@CollapseAllRows">Collapse All</Button>

<DataGrid @ref="dataGridRef"
          TItem="SelfReferenceEmployee"
          Data="@rootItems"
          ExpandTrigger="DataGridExpandTrigger.RowAndToggleClick"
          ExpandRowTrigger="OnExpandRowTrigger"
          ReadChildData="OnReadChildData"
          @bind-SelectedRow="@selectedEmployee"
          Responsive
          ShowPager>
    <DataGridColumn Field="@nameof( SelfReferenceEmployee.FullName )" Caption="Employee" Width="Width.Px( 280 )" />
    <DataGridColumn Field="@nameof( SelfReferenceEmployee.City )" Caption="City" />
    <DataGridColumn Field="@nameof( SelfReferenceEmployee.Salary )" Caption="Salary" DisplayFormat="{0:C}" />
    <DataGridCheckColumn Field="@nameof( SelfReferenceEmployee.IsActive )" Caption="Active" />
</DataGrid>
@code {
    [Inject] public EmployeeData EmployeeData { get; set; }

    private DataGrid<SelfReferenceEmployee> dataGridRef;
    private SelfReferenceEmployee selectedEmployee;
    private List<SelfReferenceEmployee> rootItems = new();
    private Dictionary<int, List<SelfReferenceEmployee>> childLookup = new();

    private bool CanToggleSelectedRow
        => selectedEmployee is not null;

    protected override async Task OnInitializedAsync()
    {
        var employees = ( await EmployeeData.GetDataAsync().ConfigureAwait( false ) ).Take( 24 ).ToList();
        BuildSelfReferenceData( employees );
        await base.OnInitializedAsync();
    }

    private void BuildSelfReferenceData( IReadOnlyList<Employee> employees )
    {
        var selfReferenceEmployees = employees.Select( x => new SelfReferenceEmployee
        {
            Id = x.Id,
            FullName = $"{x.FirstName} {x.LastName}",
            City = x.City,
            Salary = x.Salary,
            IsActive = x.IsActive,
        } ).ToList();

        var roots = selfReferenceEmployees.Take( 3 ).ToList();
        var levelOneNodes = new List<SelfReferenceEmployee>();
        var currentIndex = roots.Count;

        for ( var rootIndex = 0; rootIndex < roots.Count && currentIndex < selfReferenceEmployees.Count; rootIndex++ )
        {
            for ( var childIndex = 0; childIndex < 3 && currentIndex < selfReferenceEmployees.Count; childIndex++ )
            {
                var child = selfReferenceEmployees[currentIndex++];
                child.ParentId = roots[rootIndex].Id;
                levelOneNodes.Add( child );
            }
        }

        for ( var levelOneIndex = 0; levelOneIndex < levelOneNodes.Count && currentIndex < selfReferenceEmployees.Count; levelOneIndex++ )
        {
            for ( var childIndex = 0; childIndex < 2 && currentIndex < selfReferenceEmployees.Count; childIndex++ )
            {
                var child = selfReferenceEmployees[currentIndex++];
                child.ParentId = levelOneNodes[levelOneIndex].Id;
            }
        }

        childLookup = selfReferenceEmployees
            .Where( x => x.ParentId.HasValue )
            .GroupBy( x => x.ParentId!.Value )
            .ToDictionary( x => x.Key, x => x.ToList() );

        rootItems = selfReferenceEmployees.Where( x => !x.ParentId.HasValue ).ToList();
    }

    private bool OnExpandRowTrigger( DataGridExpandRowTriggerEventArgs<SelfReferenceEmployee> args )
    {
        args.Expandable = childLookup.ContainsKey( args.Item.Id );
        return true;
    }

    private void OnReadChildData( DataGridReadChildDataEventArgs<SelfReferenceEmployee> args )
    {
        args.Data = childLookup.TryGetValue( args.Item.Id, out var children )
            ? children
            : Enumerable.Empty<SelfReferenceEmployee>();
    }

    private Task ExpandSelectedRow()
        => selectedEmployee is null
            ? Task.CompletedTask
            : dataGridRef.ExpandRow( selectedEmployee );

    private Task CollapseSelectedRow()
        => selectedEmployee is null
            ? Task.CompletedTask
            : dataGridRef.CollapseRow( selectedEmployee );

    private Task ExpandAllRows()
        => dataGridRef.ExpandAllRows();

    private Task CollapseAllRows()
        => dataGridRef.CollapseAllRows();

    private sealed class SelfReferenceEmployee
    {
        public int Id { get; set; }

        public int? ParentId { get; set; }

        public string FullName { get; set; }

        public string City { get; set; }

        public decimal Salary { get; set; }

        public bool IsActive { get; set; }
    }
}

Row Editing

This example keeps self-reference enabled while turning on Editable rows with DataGridEditMode.Inline. Parent and child rows can both be edited.
Employee
City
Salary
Active
Samuel CollierNew Lura$86,030.41
Irvin ZiemannModestomouth$61,781.31
Gerald PollichTheresashire$58,810.75
1 - 3 of 3 items
3 items
<Button Color="Color.Success" Margin="Margin.Is2.FromEnd" Clicked="@ExpandAllRows">Expand All</Button>
<Button Color="Color.Warning" Margin="Margin.Is4.FromEnd" Clicked="@CollapseAllRows">Collapse All</Button>

<DataGrid @ref="dataGridRef"
          TItem="SelfReferenceEmployee"
          Data="@rootItems"
          ExpandTrigger="DataGridExpandTrigger.RowAndToggleClick"
          ExpandRowTrigger="OnExpandRowTrigger"
          ReadChildData="OnReadChildData"
          RowUpdated="OnRowUpdated"
          Editable
          EditMode="DataGridEditMode.Inline"
          Responsive
          ShowPager>
    <DataGridColumns>
        <DataGridColumn Field="@nameof( SelfReferenceEmployee.FullName )" Caption="Employee" Width="Width.Px( 280 )" Editable />
        <DataGridColumn Field="@nameof( SelfReferenceEmployee.City )" Caption="City" Editable />
        <DataGridNumericColumn Field="@nameof( SelfReferenceEmployee.Salary )" Caption="Salary" DisplayFormat="{0:C}" Editable />
        <DataGridCheckColumn Field="@nameof( SelfReferenceEmployee.IsActive )" Caption="Active" Editable />
        <DataGridCommandColumn NewCommandAllowed="false" DeleteCommandAllowed="false" />
    </DataGridColumns>
</DataGrid>

@if ( lastUpdatedEmployee is not null )
{
    <Alert Color="Color.Info" Margin="Margin.Is3.FromTop">
        Last updated row: <strong>@lastUpdatedEmployee.FullName</strong> (@lastUpdatedEmployee.City)
    </Alert>
}
@code {
    [Inject] public EmployeeData EmployeeData { get; set; }

    private DataGrid<SelfReferenceEmployee> dataGridRef;
    private SelfReferenceEmployee lastUpdatedEmployee;
    private List<SelfReferenceEmployee> rootItems = new();
    private Dictionary<int, List<SelfReferenceEmployee>> childLookup = new();

    protected override async Task OnInitializedAsync()
    {
        var employees = ( await EmployeeData.GetDataAsync().ConfigureAwait( false ) ).Take( 24 ).ToList();
        BuildSelfReferenceData( employees );
        await base.OnInitializedAsync();
    }

    private void BuildSelfReferenceData( IReadOnlyList<Employee> employees )
    {
        var selfReferenceEmployees = employees.Select( x => new SelfReferenceEmployee
        {
            Id = x.Id,
            FullName = $"{x.FirstName} {x.LastName}",
            City = x.City,
            Salary = x.Salary,
            IsActive = x.IsActive,
        } ).ToList();

        var roots = selfReferenceEmployees.Take( 3 ).ToList();
        var levelOneNodes = new List<SelfReferenceEmployee>();
        var currentIndex = roots.Count;

        for ( var rootIndex = 0; rootIndex < roots.Count && currentIndex < selfReferenceEmployees.Count; rootIndex++ )
        {
            for ( var childIndex = 0; childIndex < 3 && currentIndex < selfReferenceEmployees.Count; childIndex++ )
            {
                var child = selfReferenceEmployees[currentIndex++];
                child.ParentId = roots[rootIndex].Id;
                levelOneNodes.Add( child );
            }
        }

        for ( var levelOneIndex = 0; levelOneIndex < levelOneNodes.Count && currentIndex < selfReferenceEmployees.Count; levelOneIndex++ )
        {
            for ( var childIndex = 0; childIndex < 2 && currentIndex < selfReferenceEmployees.Count; childIndex++ )
            {
                var child = selfReferenceEmployees[currentIndex++];
                child.ParentId = levelOneNodes[levelOneIndex].Id;
            }
        }

        childLookup = selfReferenceEmployees
            .Where( x => x.ParentId.HasValue )
            .GroupBy( x => x.ParentId!.Value )
            .ToDictionary( x => x.Key, x => x.ToList() );

        rootItems = selfReferenceEmployees.Where( x => !x.ParentId.HasValue ).ToList();
    }

    private bool OnExpandRowTrigger( DataGridExpandRowTriggerEventArgs<SelfReferenceEmployee> args )
    {
        args.Expandable = childLookup.ContainsKey( args.Item.Id );
        return true;
    }

    private void OnReadChildData( DataGridReadChildDataEventArgs<SelfReferenceEmployee> args )
    {
        args.Data = childLookup.TryGetValue( args.Item.Id, out var children )
            ? children
            : Enumerable.Empty<SelfReferenceEmployee>();
    }

    private Task OnRowUpdated( SavedRowItem<SelfReferenceEmployee, Dictionary<string, object>> args )
    {
        lastUpdatedEmployee = args.NewItem;
        return Task.CompletedTask;
    }

    private Task ExpandAllRows()
        => dataGridRef.ExpandAllRows();

    private Task CollapseAllRows()
        => dataGridRef.CollapseAllRows();

    private sealed class SelfReferenceEmployee
    {
        public int Id { get; set; }

        public int? ParentId { get; set; }

        public string FullName { get; set; }

        public string City { get; set; }

        public decimal Salary { get; set; }

        public bool IsActive { get; set; }
    }
}

Custom Expand Template

Use column ExpandTemplate when you need full control over self-reference expand rendering. The first regular column with ExpandTemplate becomes the self-reference host, and the template context exposes Item, Level, Expandable, Expanded, and Toggle().
Employee
City
Salary
Samuel CollierNew Lura$86,030.41
Irvin ZiemannModestomouth$61,781.31
Gerald PollichTheresashire$58,810.75
1 - 3 of 3 items
3 items
<DataGrid TItem="SelfReferenceEmployee"
          Data="@rootItems"
          ExpandTrigger="DataGridExpandTrigger.ToggleClick"
          ExpandRowTrigger="OnExpandRowTrigger"
          ReadChildData="OnReadChildData"
          Responsive
          ShowPager>
    <DataGridColumns>
        <DataGridColumn Field="@nameof( SelfReferenceEmployee.FullName )" Caption="Employee" Width="Width.Px( 300 )">
            <ExpandTemplate Context="row">
                <Span Display="Display.InlineFlex" VerticalAlignment="VerticalAlignment.Middle" TextOverflow="TextOverflow.NoWrap">
                    @if ( row.Level > 0 )
                    {
                        <Span Display="Display.InlineBlock" Width="@Width.Rem( row.Level )"></Span>
                    }

                    @if ( row.Expandable )
                    {
                        <Button Size="Size.ExtraSmall"
                                Color="Color.Light"
                                Margin="Margin.Is2.FromEnd"
                                Clicked="@(() => row.Toggle())">
                            <Icon Name="@(row.Expanded ? IconName.ChevronDown : IconName.ChevronRight)" />
                        </Button>
                    }
                    else
                    {
                        <Span Display="Display.InlineBlock" Width="@Width.Rem( 1 )" Margin="Margin.Is2.FromEnd"></Span>
                    }
                </Span>
            </ExpandTemplate>
            <DisplayTemplate Context="cell">
                <Span TextWeight="TextWeight.SemiBold">@cell.DisplayValue</Span>
            </DisplayTemplate>
        </DataGridColumn>
        <DataGridColumn Field="@nameof( SelfReferenceEmployee.City )" Caption="City" />
        <DataGridNumericColumn Field="@nameof( SelfReferenceEmployee.Salary )" Caption="Salary" DisplayFormat="{0:C}" />
    </DataGridColumns>
</DataGrid>
@code {
    [Inject] public EmployeeData EmployeeData { get; set; }

    private List<SelfReferenceEmployee> rootItems = new();
    private Dictionary<int, List<SelfReferenceEmployee>> childLookup = new();

    protected override async Task OnInitializedAsync()
    {
        var employees = ( await EmployeeData.GetDataAsync().ConfigureAwait( false ) ).Take( 24 ).ToList();
        BuildSelfReferenceData( employees );
        await base.OnInitializedAsync();
    }

    private void BuildSelfReferenceData( IReadOnlyList<Employee> employees )
    {
        var selfReferenceEmployees = employees.Select( x => new SelfReferenceEmployee
        {
            Id = x.Id,
            FullName = $"{x.FirstName} {x.LastName}",
            City = x.City,
            Salary = x.Salary,
        } ).ToList();

        var roots = selfReferenceEmployees.Take( 3 ).ToList();
        var levelOneNodes = new List<SelfReferenceEmployee>();
        var currentIndex = roots.Count;

        for ( var rootIndex = 0; rootIndex < roots.Count && currentIndex < selfReferenceEmployees.Count; rootIndex++ )
        {
            for ( var childIndex = 0; childIndex < 3 && currentIndex < selfReferenceEmployees.Count; childIndex++ )
            {
                var child = selfReferenceEmployees[currentIndex++];
                child.ParentId = roots[rootIndex].Id;
                levelOneNodes.Add( child );
            }
        }

        for ( var levelOneIndex = 0; levelOneIndex < levelOneNodes.Count && currentIndex < selfReferenceEmployees.Count; levelOneIndex++ )
        {
            for ( var childIndex = 0; childIndex < 2 && currentIndex < selfReferenceEmployees.Count; childIndex++ )
            {
                var child = selfReferenceEmployees[currentIndex++];
                child.ParentId = levelOneNodes[levelOneIndex].Id;
            }
        }

        childLookup = selfReferenceEmployees
            .Where( x => x.ParentId.HasValue )
            .GroupBy( x => x.ParentId!.Value )
            .ToDictionary( x => x.Key, x => x.ToList() );

        rootItems = selfReferenceEmployees.Where( x => !x.ParentId.HasValue ).ToList();
    }

    private bool OnExpandRowTrigger( DataGridExpandRowTriggerEventArgs<SelfReferenceEmployee> args )
    {
        args.Expandable = childLookup.ContainsKey( args.Item.Id );
        return true;
    }

    private void OnReadChildData( DataGridReadChildDataEventArgs<SelfReferenceEmployee> args )
    {
        args.Data = childLookup.TryGetValue( args.Item.Id, out var children )
            ? children
            : Enumerable.Empty<SelfReferenceEmployee>();
    }

    private sealed class SelfReferenceEmployee
    {
        public int Id { get; set; }

        public int? ParentId { get; set; }

        public string FullName { get; set; }

        public string City { get; set; }

        public decimal Salary { get; set; }
    }
}

API

See the documentation below for a complete reference to all of the props and classes available to the components mentioned here.

On this page