Custom filter crashes virtualized Blazor DataGrid

I'm writing a Blazor server application which displays a Radzen DataGrid. This DataGrid utilizes both virtualization and a custom filter for one column of the DataGrid. Both of these features work with my DataGrid, but not at the same time. In other words, my implementation of virtualization works fine, as long as I'm not using custom filtering, and vice-versa.

I took a step back from my current application, and made a new/fresh Blazor server app, just to test my DataGrid setup. This involved using the default VS2019 Blazor server app template to create an application, add Radzen, and then add a simplified version of my virtualized, custom-filtered DataGrid. What I found is that it crashes with the same error I was seeing in my original application. Specifically, this is the error, as seen in Chrome dev tools:

Here's all of the code I added to the standard VS2019 Blazor server app. I simply replace the traditional "Counter" page with this:

@page "/counter"

<div>
    <RadzenDataGrid Data="@Entries" TItem="Entry" Count="@Count"
                    LoadData="@LoadData"
                    AllowFiltering="true" AllowSorting="true" AllowPaging="false"
                    AllowVirtualization="true"
                    Style="height: calc(100vh - 200px)"
                    FilterMode="FilterMode.Advanced" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive" LogicalFilterOperator="LogicalFilterOperator.And">
        <Columns>
            <RadzenDataGridColumn TItem="Entry" Property="Name" Filterable="true" Title="Name" Width="130px" />
            <RadzenDataGridColumn TItem="Entry" Property="Address" Filterable="true" Title="Address" Width="240px" FilterValue="@FilterText" FilterOperator="FilterOperator.StartsWith">
                <FilterTemplate>
                    <RadzenTextBox Name="addressEntry" @bind-Value=@FilterEntered Placeholder="Enter filter text here..." @oninput=@(args => FilterChanged(args.Value.ToString())) />
                    <RadzenTextBox Name="filterTextDisplay" @bind-Value=@FilterText ReadOnly="true" />
                </FilterTemplate>
            </RadzenDataGridColumn>
        </Columns>
    </RadzenDataGrid>
</div>

@code
{
    private string FilterEntered { get; set; } = null;
    private string FilterText { get; set; } = null;
    private int Count { get; set; }
    private IEnumerable<Entry> Entries { get; set; }
    private List<Entry> DataSource { get; set; } = new List<Entry>();

    protected override void OnInitialized()
    {
        for (var idx = 0; idx < 10000; idx++)
        {
            DataSource.Add(new Entry
            {
                Name = $"Name{idx+1}",
                Address = $"Address{idx+1}"
            });
        }
    }

    private void FilterChanged(string filter)
    {
        FilterText = filter;
    }

    private void LoadData(LoadDataArgs args)
    {
        var query = DataSource.Select(e => e).AsQueryable();

        if (!string.IsNullOrEmpty(args.Filter))
        {
            query = query.Where(args.Filter);
        }

        if (!string.IsNullOrEmpty(args.OrderBy))
        {
            query = query.OrderBy(args.OrderBy);
        }

        Entries = query.Skip(args.Skip.Value).Take(args.Top.Value).ToList();
        Count = query.Count();
    }
}
    public class Entry
    {
        public string Name { get; set; }

        public string Address { get; set; }
    }

What I'm wanting to do is "filter as I go," in that as soon as I type something into the filter box, it is sent to the bound FilterText, triggering the LoadData method with the filter.

This works in terms of triggering LoadData with what appears to be a correct filter, but it crashes with the above error. Also, if I remove the custom filter and use standard advanced filtering, what seems to be passed in to the LoadData method is virtually identical to what is coming from my custom filter, but with no resulting crash.

The FilterChanged method may seem a bit useless (and it is in this example). However, what I'm doing in my actual application is allowing someone to type in text with an English keyboard, and as they type, the text is converted in FilterChanged into polytonic Greek, which is then used as the actual filter text value. In any case, the code above demonstrates the crash.

Does anyone have any idea from looking at the error message what might be causing this? Is this a bug, or am I doing something wrong?

Hi @jeffdod,

Thanks for the report! We were able to reproduce the issue and we will do our best to provide fix before the end of the week.

Thank you for the quick response!

Hello, I'm now using version 3.13.7 of the NuGet package. I can happily report that with the test application mentioned above, the custom filtering with virtualization no longer crashes. However, I did note one oddity: when LoadData is called and a filter is applied, the effect of the filtering is not shown in the DataGrid until you reset the bound @FilterText property to null.

In other words, I enter a filter, for example, "7", LoadData is called, and I can see that the filter string is there and the query produces the correct result, but the items in the query are not what show up in the DataGrid. However, if you then backspace over the "7", resetting the query to null, the DataGrid suddenly updates and (incorrectly) shows the results of the "7" query. I say "incorrectly" because at that point, the FilterText property is now null, which means nothing should be filtered at that point.

Note that to demonstrate the query being reset to null, I changed the FilterChanged method to this:

    private void FilterChanged(string filter)
    {
        if (string.IsNullOrWhiteSpace(filter))
        {
            FilterText = null;
            return;
        }

        FilterText = filter;
    }

In any case, I would think the bug here is that LoadData is called and returns successfully with a particular query applied, and yet that query is not reflected in the DataGrid until the FilterText value is reset to null.

You can also see this clearly if you modify LoadData to this:

    private void LoadData(LoadDataArgs args)
    {
        var query = DataSource.Select(e => e).AsQueryable();

        if (!string.IsNullOrEmpty(args.Filter))
        {
            query = query.Where(args.Filter);
        }

        if (!string.IsNullOrEmpty(args.OrderBy))
        {
            query = query.OrderBy(args.OrderBy);
        }

        Entries = query.Skip(args.Skip.Value).Take(args.Top.Value).ToList();
        Count = query.Count();
        FilterText = null;
    }

This causes the filter to be immediately applied, but of course it also has the bad side effect of making it so that the text you are filtering on always disappears from the textbox.

Hi @jeffdod,

I've tried your code and it worked normally for me:
virtualfilter

except when FilterText is set to null (not sure if this is what you are reporting) - in this case you can call Reload() for the DataGrid:

virtualfilter2

Hello, I'm puzzled then, because that isn't how the same code works for me. I was using FilterMode.Advanced while you are using FilterMode.Simple, but that doesn't seem to matter in this case. I added the Reload to my FilterChanged method, and got these results:

Filter5

What I'm seeing is a delayed reaction in the filter. Note that when I type "1" into the filter box, nothing happens. Then when I change it from "1" to "12", it finally shows the search results for "1". When I enter "123", then it shows the results for "12", and only when I backspace over the "3" in "123" does it show the "123" results.

However, if you add grid.Reload() after every instance of changing the FilterText property (not just when it's set to null, as you show in your code) then it masks the issue entirely. In other words, filtering seems to work normally, but only if you call grid.Reload() every time the filter changes.

Just so we can be sure we're talking about the same thing, here is my code again in its entirety.

@page "/counter"

<div>
    <RadzenDataGrid @ref="MyGrid" Data="@Entries" TItem="Entry" Count="@Count"
                    LoadData="@LoadData"
                    AllowFiltering="true" AllowSorting="true" AllowPaging="false"
                    AllowVirtualization="true"
                    Style="height: calc(100vh - 200px)"
                    FilterMode="FilterMode.Simple" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive" LogicalFilterOperator="LogicalFilterOperator.And">
        <Columns>
            <RadzenDataGridColumn TItem="Entry" Property="Name" Filterable="true" Title="Name" Width="130px" />
            <RadzenDataGridColumn TItem="Entry" Property="Address" Filterable="true" Title="Address" Width="240px" FilterValue="@FilterText" FilterOperator="FilterOperator.Contains">
                <FilterTemplate>
                    <RadzenTextBox Name="addressEntry" @bind-Value=@FilterEntered Placeholder="Enter filter text here..." @oninput=@(args => FilterChanged(args.Value.ToString())) />
                    <RadzenTextBox Name="filterTextDisplay" @bind-Value=@FilterText ReadOnly="true" />
                </FilterTemplate>
            </RadzenDataGridColumn>
        </Columns>
    </RadzenDataGrid>
</div>

@code
{
    private string FilterEntered { get; set; } = null;
    private string FilterText { get; set; } = null;
    private int Count { get; set; }
    private IEnumerable<Entry> Entries { get; set; }
    private List<Entry> DataSource { get; set; } = new List<Entry>();
    private RadzenDataGrid<Entry> MyGrid { get; set; }

    protected override void OnInitialized()
    {
        for (var idx = 0; idx < 10000; idx++)
        {
            DataSource.Add(new Entry
            {
                Name = $"Name{idx+1}",
                Address = $"Address{idx+1}"
            });
        }
    }

    private void FilterChanged(string filter)
    {
        if (string.IsNullOrWhiteSpace(filter))
        {
            FilterText = null;
            MyGrid.Reload();
            return;
        }

        FilterText = filter;
    }

    private void LoadData(LoadDataArgs args)
    {
        var query = DataSource.Select(e => e).AsQueryable();

        if (!string.IsNullOrEmpty(args.Filter))
        {
            query = query.Where(args.Filter);
        }

        if (!string.IsNullOrEmpty(args.OrderBy))
        {
            query = query.OrderBy(args.OrderBy);
        }

        Entries = query.Skip(args.Skip.Value).Take(args.Top.Value).ToList();
        Count = query.Count();
    }
}

Honestly, calling .Reload() after each change of the FilterText property is fine with me, so I see that as an acceptable workaround. However, the developer mentality in me still wants to know why we're seeing different results with the same version of Radzen.Blazor and the same example code!

I have the same issue in the latest version 3.18.11. Has the cause ever been found?
Sorry for the late bump.

@enchev @jeffdod

BTW Was able to work around this bug in a very stupid way. It is really ugly and should not be used. I hope this will get some attention and could be fixed soon.

using Dashboard.Api;
using Dashboard.Api.Models.Debtors;
using Microsoft.AspNetCore.Components;
using Radzen;
using Radzen.Blazor;

public partial class Debug : ComponentBase
{
    private class CachedResponse
    {
        public int Skip { get; set; }
        public int Take { get; set; }
        public string? Filter { get; set; }
        public GetDebtorsResponse? Response { get; set; }
    }
    
    [Inject] public ApiClient Client { get; set; }
    
    private string FilterEntered { get; set; } = null;
    private string FilterText { get; set; } = null;
    private volatile int Count;
    private IEnumerable<Debtor> Entries { get; set; }
    private RadzenDataGrid<Debtor> MyGrid { get; set; }
    private bool _isLoading = false;
    private CancellationTokenSource? _tokenSource;
    private CachedResponse? _cachedResponse;
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);
    
    protected override async Task OnInitializedAsync()
    {
        var debtors =  await Client.GetDebtors(0, 16);
        
        if (debtors == null)
            return;

        Count = debtors.TotalCount;
        Entries = debtors.Items.ToList();
    }
    
    private async Task FilterChanged(string filter)
    {
        FilterText = string.IsNullOrWhiteSpace(filter) ? null : filter;
        
        // First reload is to set the entities
        await MyGrid.Reload();
        // Need to reload a second time to also set the count ????????? The hell
        _ = MyGrid.Reload();
    }

    private async Task LoadData(LoadDataArgs args)
    {
        _tokenSource?.Cancel();
        
        try
        {
            _tokenSource = new CancellationTokenSource();
            await _semaphore.WaitAsync(_tokenSource.Token);

            await Task.Delay(100, _tokenSource.Token);

            var skip = args.Skip ?? 0;
            var take = args.Top ?? 1;

            GetDebtorsResponse? response;

            if (_cachedResponse?.Skip == skip &&
                _cachedResponse?.Take == take &&
                _cachedResponse?.Filter == FilterText)
            {
                response = _cachedResponse.Response;
                Console.WriteLine($"Filter: {FilterText} - Count: {Count} - Cache");
            }
            else
            {
                response = await Client.GetDebtors(skip, take, FilterText, stopToken: _tokenSource.Token);
                if (response == null)
                    return;
                Console.WriteLine($"Filter: {FilterText} - Count: {response?.TotalCount ?? 0} - API");
                _cachedResponse = new CachedResponse
                {
                    Skip = skip,
                    Take = take,
                    Filter = FilterText,
                    Response = response
                };
            }

            if (response == null)
                return;

            Count = response.TotalCount;

            Entries = response.Items.ToList();

            // Why o why does this fix stuff
            _ = Task.Delay(100, _tokenSource.Token).ContinueWith(t =>
            {
                InvokeAsync(StateHasChanged);
            });
        }
        catch (Exception)
        {
            //Ignore
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

We can't fix this issue until we have a reproduction. If you can provide a code snippet which we can run please share it with us.

Hereby an example

@page "/debug"
@using System.Linq;
@using System.Linq.Dynamic.Core;
@using Microsoft.AspNetCore.Components;

<div>
    <RadzenDataGrid @ref="MyGrid" Data="@Entries" TItem="Entry" Count="@Count"
                    LoadData="@LoadData"
                    AllowFiltering="true" AllowSorting="true" AllowPaging="false"
                    AllowVirtualization="true"
                    Style="height: calc(100vh - 500px)"
                    FilterMode="FilterMode.Simple" FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive" LogicalFilterOperator="LogicalFilterOperator.And">
        <Columns>
            <RadzenDataGridColumn TItem="Entry" Property="Name" Filterable="true" Title="Name" Width="130px" />
            <RadzenDataGridColumn TItem="Entry" Property="Address" Filterable="true" Title="Address" Width="240px" FilterValue="@FilterText" FilterOperator="FilterOperator.Contains">
                <FilterTemplate>
                    <RadzenTextBox Name="addressEntry" @bind-Value=@FilterEntered Placeholder="Enter filter text here..." @oninput=@(args => FilterChanged(args.Value.ToString())) />
                    <RadzenTextBox Name="filterTextDisplay" @bind-Value=@FilterText ReadOnly="true" />
                </FilterTemplate>
            </RadzenDataGridColumn>
        </Columns>
    </RadzenDataGrid>
</div>

@code {

    private class Entry
    {
        public string Name { get; set; }
        public string Address { get; set; }
    }
    
    private string FilterEntered { get; set; } = null;
    private string FilterText { get; set; } = null;
    private int Count { get; set; }
    private IEnumerable<Entry> Entries { get; set; }
    private List<Entry> DataSource { get; set; } = new List<Entry>();
    private RadzenDataGrid<Entry> MyGrid { get; set; }
    private bool _isLoading = false;

    protected override void OnInitialized()
    {
        for (var idx = 0; idx < 10000; idx++)
        {
            DataSource.Add(new Entry
            {
                Name = $"Name{idx+1}",
                Address = $"Address{idx+1}"
            });
        }
    }

    private async Task FilterChanged(string filter)
    {
        _isLoading = true;
        FilterText = string.IsNullOrWhiteSpace(filter) ? null : filter;
    }

    private async Task LoadData(LoadDataArgs args)
    {
        var entries = await GetEntries(args.Skip ?? 0, args.Top ?? 0, args.Filter);

        Entries = entries;
        Count = string.IsNullOrEmpty(args.Filter) ? DataSource.AsQueryable().Count() : DataSource.AsQueryable().Where(args.Filter).Count();

        StateHasChanged();
    }

    private async Task<List<Entry>> GetEntries(int skip, int take, string? filter)
    {
        var query = DataSource.AsQueryable();

        if (!string.IsNullOrEmpty(filter))
        {
            query = query.Where(filter);
        }

    //Simulate api delay
        await Task.Delay(200);

        return query.Skip(skip).Take(take).ToList();
    }
}

I was able to reproduce it on multiple computers with different specs

What should I do with this example in order to reproduce the exception? I tried filtering but it seems to work as expected.
filter

I'll create a whole project for you to download and try out once I get the time to sit-down and make it.

But basically I have a server side, server prerenderd app. When I use a custom filter in a virtualized datagrid it will refresh the datagrid before loading in the new data. Thus always filtering with the previous filter settings.

I was able to force radzen to show correct the entries by reloading the grid onfilter. The problem then was that while the entries did update the count didn't. That is why the second reload is there.

In the end it did end up working with a lot of hacks as you might have seen.

I'll return once I have a full project for you and will probably also add a video or 2.

Hereby the project and a video of the issue. I hope you will be able to reproduce it.

I am not able to upload directly to the form as I am a new user so here is a download link. Though it will expire in 48 hours. https://a.tmp.ninja/psfBtoNK.zip

Hereby the video where I show the issue.

This thread is about an exception being thrown. I can't reproduce any exception in your application nor can I understand what the problem you are trying to show is.

I stand corrected - I saw what the problem you are trying to depict is - the DataGrid does not show the up to date result and is one or two filters behind.

Yes indeed.
Though sometimes even the Count will fall behind 2 filters and Entries will fall behind just 1.
It seems to be a problem with how load data is called and that loaddata will finish after the grid will reload. But that is just a hunch.

@TheNoNinja you can avoid this if you execute Reload() on filter change:

    private async Task FilterChanged(string filter)
    {
        _isLoading = true;
        FilterText = string.IsNullOrWhiteSpace(filter) ? null : filter;
        await MyGrid.Reload();
    }

While that does show the correct result there is still a problem.

Try typing "1234". That should give 1 result. But you can still scroll. The Datagrid thinks there is still more than one line. If you backspace one till you filter on "123" it will only show one result as if you were filtering on "1234".

It seems that the Count is also a filter behind.

This was why I am reloading the grid twice in this example.

private async Task FilterChanged(string filter)
    {
        FilterText = string.IsNullOrWhiteSpace(filter) ? null : filter;
        
        // First reload is to set the entities
        await MyGrid.Reload();
        // Need to reload a second time to also set the count ????????? The hell
        _ = MyGrid.Reload();
    }

Hereby an video showing this bug.

The number in red is the Count passed to radzen in loaddata

This will be fixed in our next update before the end of the week.

Thank you for looking into it! :smiley: