Incorrect Use of IEnumerable<T> in Radzen Tutorials Causes Logic Failures in Real-World Scenarios #1914

Closed
opened 2026-01-29 18:00:13 +00:00 by claunia · 5 comments
Owner

Originally created by @iustin94 on GitHub (Nov 27, 2025).

Description

While implementing a Radzen component following the official tutorials, I encountered logic failures caused by using IEnumerable<T> for data binding. Specifically in my case it was when implementing a data grid with a hierarchy inside it.

Radzen examples frequently use IEnumerable<T> as the default type in their tutorials, but in many real-world scenarios (editing, indexing, reordering, etc.), IEnumerable<T> is not appropriate.

My code followed the examples, and it resulted in unexpected behavior because the sequence was:

  • lazily evaluated
  • re-executed on every enumeration
  • non-indexable
  • non-mutable

Switching the data source to IList<T> / List<T> immediately resolved the issues.

This is not a runtime bug in Radzen itself — it is a documentation issue that can lead to subtle and hard-to-diagnose bugs for users.


Steps to Reproduce

  1. Bind Radzen components (such as a grid or editable list) to an IEnumerable<T> as shown in existing tutorials.
  2. Perform operations involving:
    • indexing
    • stable ordering
    • modifying the underlying collection
    • enumerating the data more than once per render
  3. Observe inconsistent or incorrect behavior due to lazy evaluation or missing list semantics.

Switching to List<T> or IList<T> resolves the problem.


Expected Behavior

Radzen tutorials should recommend and demonstrate:

  • List<T> / IList<T> for editable or index-based scenarios
  • IEnumerable<T> only for simple read-only data binding

Actual Behavior

Tutorials consistently use IEnumerable<T>, which:

  • obscures important semantic differences
  • encourages patterns that break when components rely on indexing or mutation
  • causes repeated execution of queries on each enumeration
  • leads to confusing component state behavior, especially for new users

Suggested Fix

Update Radzen documentation/tutorials to clarify:

  • When IEnumerable<T> is appropriate
  • When List<T>, IList<T>, or ObservableCollection<T> should be used
  • How lazy evaluation interacts with Radzen/Blazor component rendering
  • Why materializing sequences (e.g., with .ToList()) improves UI stability

A short section like “Choosing the Right Collection Type for Radzen Components” would greatly help.

Originally created by @iustin94 on GitHub (Nov 27, 2025). ### Description While implementing a Radzen component following the official tutorials, I encountered logic failures caused by using `IEnumerable<T>` for data binding. Specifically in my case it was when implementing a data grid with a hierarchy inside it. Radzen examples frequently use `IEnumerable<T>` as the default type in their tutorials, but in many real-world scenarios (editing, indexing, reordering, etc.), `IEnumerable<T>` is not appropriate. My code followed the examples, and it resulted in unexpected behavior because the sequence was: - lazily evaluated - re-executed on every enumeration - non-indexable - non-mutable Switching the data source to `IList<T>` / `List<T>` immediately resolved the issues. This is not a runtime bug in Radzen itself — it is a **documentation issue** that can lead to subtle and hard-to-diagnose bugs for users. --- ## Steps to Reproduce 1. Bind Radzen components (such as a grid or editable list) to an `IEnumerable<T>` as shown in existing tutorials. 2. Perform operations involving: - indexing - stable ordering - modifying the underlying collection - enumerating the data more than once per render 3. Observe inconsistent or incorrect behavior due to lazy evaluation or missing list semantics. Switching to `List<T>` or `IList<T>` resolves the problem. --- ## Expected Behavior Radzen tutorials should recommend and demonstrate: - `List<T>` / `IList<T>` for editable or index-based scenarios - `IEnumerable<T>` only for simple read-only data binding --- ## Actual Behavior Tutorials consistently use `IEnumerable<T>`, which: - obscures important semantic differences - encourages patterns that break when components rely on indexing or mutation - causes repeated execution of queries on each enumeration - leads to confusing component state behavior, especially for new users --- ## Suggested Fix Update Radzen documentation/tutorials to clarify: - When `IEnumerable<T>` is appropriate - When `List<T>`, `IList<T>`, or `ObservableCollection<T>` should be used - How lazy evaluation interacts with Radzen/Blazor component rendering - Why materializing sequences (e.g., with `.ToList()`) improves UI stability A short section like **“Choosing the Right Collection Type for Radzen Components”** would greatly help.
Author
Owner

@akorchev commented on GitHub (Nov 27, 2025):

Hi @iustin94,

Can you clarify what "Observe inconsistent or incorrect behavior due to lazy evaluation or missing list semantics." means? Provide a a concrete example that shows a problem you have experienced with IEnumerable.

@akorchev commented on GitHub (Nov 27, 2025): Hi @iustin94, Can you clarify what "Observe inconsistent or incorrect behavior due to lazy evaluation or missing list semantics." means? Provide a a concrete example that shows a problem you have experienced with IEnumerable<T>.
Author
Owner

@iustin94 commented on GitHub (Nov 27, 2025):

In my case the issue was that when I expanded the row, the _grid.ExpandRows() would get called as expected, then in the UI I would see that the row would get expanded for a fraction of a second but afterwards it would return to closed state before the child element would even be rendered. I did some debugging and saw that the "RowCollapse" callback was not called to be the cause of the issue.

As it turned out, my issue was that by following the tutorials and using the IEnumerable type for the _grid.Data parameter, this caused the row items to not have a stable identity in memory as I suspect which made the grid rerender with "new" rows and loose track of the previous one, resulting in the row expansion to fail :) hope this helps in clarifying it.

@iustin94 commented on GitHub (Nov 27, 2025): In my case the issue was that when I expanded the row, the _grid.ExpandRows() would get called as expected, then in the UI I would see that the row would get expanded for a fraction of a second but afterwards it would return to closed state before the child element would even be rendered. I did some debugging and saw that the "RowCollapse" callback was not called to be the cause of the issue. As it turned out, my issue was that by following the tutorials and using the IEnumerable type for the _grid.Data parameter, this caused the row items to not have a stable identity in memory as I suspect which made the grid rerender with "new" rows and loose track of the previous one, resulting in the row expansion to fail :) hope this helps in clarifying it.
Author
Owner

@akorchev commented on GitHub (Nov 27, 2025):

I am sorry but I still don't understand much. I suspect however that the value you were passing to the Data of RadzenDataGrid was changing every time. This isn't a supported case and would have the same effect regardless of the type of the data. Can't tell much until I see some actual code or reproduction.

@akorchev commented on GitHub (Nov 27, 2025): I am sorry but I still don't understand much. I suspect however that the value you were passing to the Data of RadzenDataGrid was changing every time. This isn't a supported case and would have the same effect regardless of the type of the data. Can't tell much until I see some actual code or reproduction.
Author
Owner

@iustin94 commented on GitHub (Nov 27, 2025):

Here is a sample table that reproduces the issue I believe. the _grid.ExpandRows() ends up getting called but the table does not reflect that.

I'm not sure what the difference is when you say that "the Data of RadzenDataGrid was changing over time" is between my use case and the usecase of any data input component, or the ones shown in the tutorials. The table gets a list of dto's, it the user performs an action in the table and so the front end sends a command to the back end, then the table is re-rendered with updated data.

Seems like a usecase that all the tutorials on the website address?

`
@page "/grid-repro"
@using System.Diagnostics
@using Radzen
@using Radzen.Blazor

RadzenDataGrid expansion repro: IEnumerable vs List

<RadzenDataGrid @ref="grid"
Data="Rows"
TItem="RowDto"
RowRender="OnRowRender"
RowExpand="OnRowExpand"
RowCollapse="OnRowCollapse"
LoadChildData="LoadChildData"
AllowFiltering="false"
AllowSorting="false"
AllowColumnResize="false"
AllowColumnReorder="false">


@row.Name


@code {
private RadzenDataGrid? grid;

private IEnumerable<RowDto> Rows => GetRowsDeferred();

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender && grid is not null && Rows is not null)
    {
        // Expand rows where the model says IsExpanded = true
        var toExpand = Rows.Where(r => r.IsExpanded).ToList();
        Debug.WriteLine($"[repro] First render: expanding {toExpand.Count} row(s)");
        if (toExpand.Count > 0)
        {
            // Important: this uses the instances from the current 'Rows' enumeration
            await grid.ExpandRows(toExpand);
        }
    }
}

private void OnRowRender(RowRenderEventArgs<RowDto> args)
{
    args.Expandable = true;
    Debug.WriteLine($"[repro] RowRender id={args.Data.Id} isExpanded={args.Data.IsExpanded}");
}

private void LoadChildData(DataGridLoadChildDataEventArgs<RowDto> args)
{
    args.Data = args.Item.Children;
}

private Task OnRowExpand(RowDto item)
{
    Debug.WriteLine($"[repro] RowExpand id={item.Id} (before) IsExpanded={item.IsExpanded}");
    item.IsExpanded = true;
    Debug.WriteLine($"[repro] RowExpand id={item.Id} (after) IsExpanded={item.IsExpanded}");
    return Task.CompletedTask;
}

private Task OnRowCollapse(RowDto item)
{
    Debug.WriteLine($"[repro] RowCollapse id={item.Id}");
    item.IsExpanded = false;
    return Task.CompletedTask;
}

private IEnumerable<RowDto> GetRowsDeferred()
{
    // Parent A with one child
    var aId = Guid.Parse("00000000-0000-0000-0000-00000000000A");
    var a = new RowDto
    {
        Id = aId,
        Name = "Parent A",
        IsExpanded = true,
        Children = new List<RowDto>
        {
            new RowDto { Id = Guid.Parse("00000000-0000-0000-0000-0000000000A1"), Name = "A.1" }
        }
    };

    // Parent B with two children
    var bId = Guid.Parse("00000000-0000-0000-0000-00000000000B");
    var b = new RowDto
    {
        Id = bId,
        Name = "Parent B",
        IsExpanded = false,
        Children = new List<RowDto>
        {
            new RowDto { Id = Guid.Parse("00000000-0000-0000-0000-0000000000B1"), Name = "B.1" },
            new RowDto { Id = Guid.Parse("00000000-0000-0000-0000-0000000000B2"), Name = "B.2" }
        }
    };

    var seq = new[] { a, b }.OrderBy(x => x.Name);
    foreach (var r in seq)
        yield return Clone(r);
}

private static RowDto Clone(RowDto r)
{
    return new RowDto
    {
        Id = r.Id,
        Name = r.Name,
        IsExpanded = r.IsExpanded,
        Children = r.Children.Select(c => new RowDto
        {
            Id = c.Id,
            Name = c.Name,
            IsExpanded = c.IsExpanded,
            Children = new List<RowDto>()
        }).ToList()
    };
}

public sealed class RowDto
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public bool IsExpanded { get; set; }
    public List<RowDto> Children { get; set; } = new();
}

}`

@iustin94 commented on GitHub (Nov 27, 2025): Here is a sample table that reproduces the issue I believe. the _grid.ExpandRows() ends up getting called but the table does not reflect that. I'm not sure what the difference is when you say that "the Data of RadzenDataGrid was changing over time" is between my use case and the usecase of any data input component, or the ones shown in the tutorials. The table gets a list of dto's, it the user performs an action in the table and so the front end sends a command to the back end, then the table is re-rendered with updated data. Seems like a usecase that all the tutorials on the website address? ` @page "/grid-repro" @using System.Diagnostics @using Radzen @using Radzen.Blazor <h3>RadzenDataGrid expansion repro: IEnumerable vs List</h3> <RadzenDataGrid @ref="grid" Data="Rows" TItem="RowDto" RowRender="OnRowRender" RowExpand="OnRowExpand" RowCollapse="OnRowCollapse" LoadChildData="LoadChildData" AllowFiltering="false" AllowSorting="false" AllowColumnResize="false" AllowColumnReorder="false"> <Columns> <RadzenDataGridColumn TItem="RowDto" Title="Name"> <Template Context="row">@row.Name</Template> </RadzenDataGridColumn> </Columns> </RadzenDataGrid> @code { private RadzenDataGrid<RowDto>? grid; private IEnumerable<RowDto> Rows => GetRowsDeferred(); protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender && grid is not null && Rows is not null) { // Expand rows where the model says IsExpanded = true var toExpand = Rows.Where(r => r.IsExpanded).ToList(); Debug.WriteLine($"[repro] First render: expanding {toExpand.Count} row(s)"); if (toExpand.Count > 0) { // Important: this uses the instances from the current 'Rows' enumeration await grid.ExpandRows(toExpand); } } } private void OnRowRender(RowRenderEventArgs<RowDto> args) { args.Expandable = true; Debug.WriteLine($"[repro] RowRender id={args.Data.Id} isExpanded={args.Data.IsExpanded}"); } private void LoadChildData(DataGridLoadChildDataEventArgs<RowDto> args) { args.Data = args.Item.Children; } private Task OnRowExpand(RowDto item) { Debug.WriteLine($"[repro] RowExpand id={item.Id} (before) IsExpanded={item.IsExpanded}"); item.IsExpanded = true; Debug.WriteLine($"[repro] RowExpand id={item.Id} (after) IsExpanded={item.IsExpanded}"); return Task.CompletedTask; } private Task OnRowCollapse(RowDto item) { Debug.WriteLine($"[repro] RowCollapse id={item.Id}"); item.IsExpanded = false; return Task.CompletedTask; } private IEnumerable<RowDto> GetRowsDeferred() { // Parent A with one child var aId = Guid.Parse("00000000-0000-0000-0000-00000000000A"); var a = new RowDto { Id = aId, Name = "Parent A", IsExpanded = true, Children = new List<RowDto> { new RowDto { Id = Guid.Parse("00000000-0000-0000-0000-0000000000A1"), Name = "A.1" } } }; // Parent B with two children var bId = Guid.Parse("00000000-0000-0000-0000-00000000000B"); var b = new RowDto { Id = bId, Name = "Parent B", IsExpanded = false, Children = new List<RowDto> { new RowDto { Id = Guid.Parse("00000000-0000-0000-0000-0000000000B1"), Name = "B.1" }, new RowDto { Id = Guid.Parse("00000000-0000-0000-0000-0000000000B2"), Name = "B.2" } } }; var seq = new[] { a, b }.OrderBy(x => x.Name); foreach (var r in seq) yield return Clone(r); } private static RowDto Clone(RowDto r) { return new RowDto { Id = r.Id, Name = r.Name, IsExpanded = r.IsExpanded, Children = r.Children.Select(c => new RowDto { Id = c.Id, Name = c.Name, IsExpanded = c.IsExpanded, Children = new List<RowDto>() }).ToList() }; } public sealed class RowDto { public Guid Id { get; set; } public string Name { get; set; } = string.Empty; public bool IsExpanded { get; set; } public List<RowDto> Children { get; set; } = new(); } }`
Author
Owner

@akorchev commented on GitHub (Nov 27, 2025):

Yes, this is precisely the issue I was talking about - new instance returned every time. We do not support such scenarios as there is no way to check if the data source has changed or not.

@akorchev commented on GitHub (Nov 27, 2025): Yes, this is precisely the issue I was talking about - new instance returned every time. We do not support such scenarios as there is no way to check if the data source has changed or not.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/radzen-blazor#1914