DataGrid settings and filters bug

Hello,

(latest version, wasm)

I'm stroring grid's settings using @bind-Settings to app's client state. Now, after i filter the grid with one or more columns, saved state is updated and looks "ok", that means columns array contains columns and those with filter have it set.

However, when i return back to component, settings get loaded (brakepoint is being hit...), still looks "ok", grid is rendered with filters set, so visually everything looks right. After that LoadData event get dispatched and it does not contains any filters, which of course results in loading data without filters.

Hey @ns6000,

I've just combined our examples for LoadData binding and Save/Load settings and everything worked normally:

datagrid-loaddata-settings

Here is the code:

@page "/datagrid-loaddata"
@using System.Linq.Dynamic.Core
@using RadzenBlazorDemos.Data
@using RadzenBlazorDemos.Models.Northwind
@using Microsoft.JSInterop
@using System.Text.Json

@inject IJSRuntime JSRuntime
@inherits DbContextPage

<RadzenText TextStyle="TextStyle.H3" TagName="TagName.H1" Class="my-4">
    DataGrid <strong>LoadData</strong>
</RadzenText>
<RadzenText TextStyle="TextStyle.Body1" Class="my-4">
    The <code>LoadData</code> event allows you to perform custom paging, sorting and filtering.
</RadzenText>

<RadzenExample Name="DataGridLoadData" Heading="false" Documentation="false">
    <RadzenButton Text="Reset" Click="@Reset" Style="margin-bottom: 20px;" />
    <RadzenDataGrid @bind-Settings="@Settings" style="height: 335px" @ref="grid" IsLoading=@isLoading Count="@count" Data="@employees" LoadData="@LoadData"
                    AllowSorting="true" AllowFiltering="true" FilterMode="FilterMode.Simple" AllowPaging="true" PageSize="4" PagerHorizontalAlign="HorizontalAlign.Center" TItem="Employee" ColumnWidth="200px">
        <Columns>
            <RadzenDataGridColumn TItem="Employee" Property="EmployeeID" Filterable="false" Title="ID" Frozen="true" Width="80px" TextAlign="TextAlign.Center" />
            <RadzenDataGridColumn TItem="Employee" Title="Photo" Frozen="true" Sortable="false" Filterable="false" Width="80px" TextAlign="TextAlign.Center" >
                <Template Context="data">
                    <RadzenImage Path="@data.Photo" class="rz-gravatar" />
                </Template>
            </RadzenDataGridColumn>
            <RadzenDataGridColumn TItem="Employee" Property="FirstName" Title="First Name" Frozen="true" Width="160px"/>
            <RadzenDataGridColumn TItem="Employee" Property="LastName" Title="Last Name" Width="160px"/>
            <RadzenDataGridColumn TItem="Employee" Property="Title" Title="Job Title" 
                Type="typeof(IEnumerable<string>)" FilterValue="@selectedTitles" FilterOperator="FilterOperator.Contains" Width="200px">
                <FilterTemplate>
                    <RadzenDropDown @bind-Value=@selectedTitles Style="width:100%"
                        Change=@OnSelectedTitlesChange Data="@(titles)" AllowClear="true" Multiple="true" />
                </FilterTemplate>
            </RadzenDataGridColumn>
            <RadzenDataGridColumn TItem="Employee" Property="TitleOfCourtesy" Title="Title" Width="120px" />
            <RadzenDataGridColumn TItem="Employee" Property="BirthDate" Title="Birth Date" FormatString="{0:d}" Width="160px" />
            <RadzenDataGridColumn TItem="Employee" Property="HireDate" Title="Hire Date" FormatString="{0:d}" Width="160px" />
            <RadzenDataGridColumn TItem="Employee" Property="Address" Title="Address" Width="200px" />
            <RadzenDataGridColumn TItem="Employee" Property="City" Title="City" Width="160px" />
            <RadzenDataGridColumn TItem="Employee" Property="Region" Title="Region" Width="160px" />
            <RadzenDataGridColumn TItem="Employee" Property="PostalCode" Title="Postal Code" Width="160px" />
            <RadzenDataGridColumn TItem="Employee" Property="Country" Title="Country" Width="160px" />
            <RadzenDataGridColumn TItem="Employee" Property="HomePhone" Title="Home Phone" Width="160px" />
            <RadzenDataGridColumn TItem="Employee" Property="Extension" Title="Extension" Width="160px" />
            <RadzenDataGridColumn TItem="Employee" Property="Notes" Title="Notes" Width="300px" />
        </Columns>
    </RadzenDataGrid>
</RadzenExample>

<RadzenCard class="mt-4">
    <RadzenText TextStyle="TextStyle.H6" TagName="TagName.H2" Class="mb-3">Perform custom data-binding</RadzenText>
    <RadzenText TextStyle="TextStyle.Body1">
        1. Set the Data and Count properties.
    </RadzenText>
        <pre class="mt-3 p-3">
            <code>&lt;RadzenDataGrid Count="@@count" Data="@@employees"</code>
        </pre>
    <RadzenText TextStyle="TextStyle.Body1">
        2. Handle the LoadData event and update the Data and Count backing fields (<code>employees</code> and <code>count</code> in this case).
    </RadzenText>
        <pre class="mt-3 p-3">
            <code>
void LoadData(LoadDataArgs args)
{
    var query = dbContext.Employees.AsQueryable();

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

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

    count = query.Count();

    employees = query.Skip(args.Skip.Value).Take(args.Top.Value).ToList();


} 
            </code>
        </pre>
</RadzenCard>
@code {
    RadzenDataGrid<Employee> grid;
    int count;
    IEnumerable<Employee> employees;
    bool isLoading = false;

    List<string> titles = new List<string> {"Sales Representative", "Vice President, Sales", "Sales Manager", "Inside Sales Coordinator" };
    IEnumerable<string> selectedTitles;

    async Task OnSelectedTitlesChange(object value)
    {
        if (selectedTitles != null && !selectedTitles.Any())
        {
            selectedTitles = null;  
        }
        
        await grid.FirstPage();
    }

    async Task Reset()
    {
        grid.Reset(true); 
        await grid.FirstPage(true);
    }

    async Task LoadData(LoadDataArgs args)
    {
        isLoading = true;

        await Task.Yield();

        // This demo is using https://dynamic-linq.net
        var query = dbContext.Employees.AsQueryable();

        if (!string.IsNullOrEmpty(args.Filter))
        {
            // Filter via the Where method
            query = query.Where(args.Filter);
        }

        if (!string.IsNullOrEmpty(args.OrderBy))
        {
            // Sort via the OrderBy method
            query = query.OrderBy(args.OrderBy);
        }

        // Important!!! Make sure the Count property of RadzenDataGrid is set.
        count = query.Count();

        // Perform paginv via Skip and Take.
        employees = query.Skip(args.Skip.Value).Take(args.Top.Value).ToList();

        isLoading = false;
    }

    DataGridSettings _settings;
    public DataGridSettings Settings
    {
        get
        {
            return _settings;
        }
        set
        {
            if (_settings != value)
            {
                _settings = value;
                InvokeAsync(SaveStateAsync);
            }
        }
    }

    private async Task LoadStateAsync()
    {
        await Task.CompletedTask;

        var result = await JSRuntime.InvokeAsync<string>("window.localStorage.getItem", "Settings");
        if (!string.IsNullOrEmpty(result))
        {
            _settings = JsonSerializer.Deserialize<DataGridSettings>(result);
        }
    }

    private async Task SaveStateAsync()
    {
        await Task.CompletedTask;

        await JSRuntime.InvokeVoidAsync("eval", $@"window.localStorage.setItem('Settings', '{JsonSerializer.Serialize<DataGridSettings>(Settings)}')");
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await LoadStateAsync();
            StateHasChanged();
        }
    }
}

I'm probably doing something wrong, but i'm not sure what :frowning:

I'm trying to create my own "default" typed datagrid component, which basically means that my component contains instance of component RadzenDataGrid with many attributes set to my prefered values and few parameters.

Shortened important parts are as follows:

@typeparam T


<RadzenDataGrid
	TItem="@T"
	Data="@data"
	Count="@dataTotal"
	LoadData="@OnLoadData"
	@bind-Settings="@MySettings"
	...
>
	...
</RadzenDataGrid>


@code {
	[Parameter] public Func<LoadDataArgs, Task<ListWithTotalCount<T>>>? DataLoader { get; set; }

	[Parameter] public EventCallback<DataGridSettings> MySettingsChanged { get; set; }
	[Parameter] public DataGridSettings? MySettings {
		get => mySettings;
		set {
			if(mySettings != value)
				MySettingsChanged.InvokeAsync(mySettings = value);
		}
	}

	private List<T>? data;
	private int dataTotal = 0;
	private DataGridSettings? mySettings;

	private async Task OnLoadData(LoadDataArgs params)
	{
		ListWithTotalCount<T>? listWithTotalCount = DataLoader is not null
			? await DataLoader.Invoke(params)
			: null;

		if(listWithTotalCount is not null)
		{
			data = listWithTotalCount.List;
			dataTotal = listWithTotalCount.Total;
		}
	}
}

Point of all of this is, that I'll reuse my grid component somewhere and just add specific method implementation to DataLoader property and bind to DataGridSettings instance with stored settings. Until i integrated settings, everything worked fine, and can work fine again, as soon as i remove @bind-Settings="@MySettings" (effectively removing settings functionality).

But with settings binding present things start to fall apart. For example:

  • enum filtering behaves strangely, i can select one of the values in filter dropdown, list will filter, but then i cannot remove or change the dropdown value, i can do it, but nothing happens (even the dropdown won't re-render)
  • if i set some (text) filter column, data will get filtered but if i navigate out of component and back, filter is set but data won't get filtered

I tried to combine LoadData with Settings examples as you mentioned, and it really works, so i presume, problem is somewhere with Settings binding in my component. What i'm trying to achieve is basically to delegate RadzenDataGrid Settings binding to my component.

With setup like this, when I navigate back to the component I can see that even in OnInitialized event, the private DataGridSettings? mySettings is already populated with settings (and filters). But breakpoint in OnLoadData and its params argument does not reflect those settings (which are still present in mySettings btw).

Now I wasted several hours with trying to debug this... Still no idea why it doesn't work. So I made completely new page, used RadzeDataGrid directly and it works. Next step I did was to get my Grid component which is encapsulating RadzenDataGrid as close as possible to this working example. I honestly think, those two instances are the same as far as i can tell and believe me i spent hours looking at them. Only thing I was able to get out of this was, that when I run working example I'll get following output:

  • OnLoadDataEvent(LoadDataArgs args)
    args.Filters.Count(): 0
    MySettings?.Columns.Count(x => x.FilterValue is not null):
  • OnAfterRenderAsync(firstRender = True)
  • OnLoadDataEvent(LoadDataArgs args)
    args.Filters.Count(): 2
    MySettings?.Columns.Count(x => x.FilterValue is not null): 2

But when I'll run my Grid component I'll get this instead:

  • OnAfterRenderAsync(firstRender = True)
  • OnLoadDataEvent(LoadDataArgs args) / args.Filters.Count(): 0 / MySettings?.Columns.Count(x => x.FilterValue is not null): 1

So, by entering the component, i won't get the first LoadData event without filters, but I'll get the second one, cause by StateHasChanged() in OnAfterRenderAsync. In the second LoadData event, binded MySettings property already contains filtering data but the args instance does not.

I have no idea, why are these sequences of events different :frowning:

I am experiencing basically the same issue while using bind-settings and LoadData in a Blazor Server app. I've duplicated the code posted by @enchev and I'm still not able to get the LoadData method to fire with the correct LoadDataArg values. Using the code posted by @enchev, the I get the following sequence of events:

  • LoadData(LoadDataArgs args) - args contains no filters, sorting, paging, etc.
  • OnAfterRenderAsync(firstRender = True)
  • LoadStateAsync()

I would expect LoadData to be called again after the settings have been loaded via LoadStateAsync, but it never does. The datagrid settings appear to be loaded because the grid is showing the correct sorting, filters, and paging, but the data is not correct because LoadData method is not getting called again after the settings have been loaded. I have tried calling grid.Reload() after LoadStateAsync, but the LoadData call's args still do not contain any filters, paging or sorting.

I assume the expected behavior would be for LoadData to be called initially without any LoadDataArgs and then be called again after the bind-settings have been set via LoadStateAsync(). If so, this does not appear to be happening for me.

1 Like

I would argue, that ideally, LoadData would fire only once, with correct LoadDataArgs, because now, in @enchev 's working example, it's dispatched twice, which means two db queries or API calls. First without desired parameters and second one with correct ones.

But at this point I don't really care about one more DB / API call anymore, I would be happy if I could get my Grid to work at all. That means to actually get the LoadData event with correct parameters.

@ns6000, is LoadData firing the second time for you after you load the settings from localstorage? I agree that LoadData should ideally only fire once, but my plan was to add logic to prevent the initial API call in the first LoadData and only allow it after the grid settings have been set. My problem is that I'm not getting a second LoadData call after loading the settings from localstorage.

@ghck17
As I posted earlier, I'm getting LoadData event only once, after OnAfterRenderAsync, which loads Settings from local storage and calls StateHasChanged.

After more debugging, it appears I am only experiencing my issue when I have a filter applied. I am using advanced filtering and when I have a filter applied, LoadData is not getting called after LoadStateAsync(). With only sorting and paging applied, I'm seeing the expected behavior and everything functions properly. @enchev, can you try applying an advanced filter in your above example and confirm there is not a bug around this? TIA.

Interesting, I can confirm this, as soon as filters are completely disabled, it works. Also I've tried different filter modes and when I'm using Simple filter mode and I'll set filter on column with enums (filter is rendered as dropdown then) to some value, then another filter on decimal column, I can't change or modify the enum filter anymore. I can do it but the change is ignored and dropdown stays at the first choice. But, if I switch to the Advanced filter mode, manipulating filters works as expected, meaning I can set / reset / remove multiple filters and that works, but the LoadData with wrong arguments problem remains.

Here is the same code I've posted in this thread however with advanced filter. LoadData is execute when settings are loaded with proper values for filtering:

I've created new .net 6 WASM project, then i added Radzen, then i took the example from docs, replaced with it default Index component and removed dependency on db and EventConsole component.

Instead, I've added some console logging and LoadData method. Everything works, now only change necessary to make the example stop working competely, is to initialize model to empty list.

So, to change:
private IEnumerable<Employee>? employees;

to:
private IEnumerable<Employee>? employees = new List<Employee>()

@enchev does it help in any way?

Complete code of the Index component:

@page "/"
@using Radzen
@using Radzen.Blazor
@using Microsoft.JSInterop
@using System.Text.Json

@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager

<RadzenButton Click="@(args => Settings = null)" Text="Clear saved settings" Style="margin-bottom: 16px" />
<RadzenButton Click="@(args => NavigationManager.NavigateTo("/", true))" Text="Reload" Style="margin-bottom: 16px" />

<RadzenDataGrid
	AllowFiltering="true"
	AllowColumnPicking="true"
	AllowGrouping="true"
	AllowPaging="true"
	PageSize="5"
	AllowSorting="true"
	AllowMultiColumnSorting="true"
	ShowMultiColumnSortingIndex="true"
	AllowColumnResize="true"
	FilterCaseSensitivity="@FilterCaseSensitivity.CaseInsensitive"
	FilterMode="@FilterMode.Simple"
	TItem="Employee"
	Data="@employees"
	Count="@totalCount"
	LoadData="@OnLoadData"
	@bind-Settings="@Settings"
>
	<Columns>
		<RadzenDataGridColumn TItem="Employee"	Property="FirstName"	Title="First name" />
		<RadzenDataGridColumn TItem="Employee"	Property="LastName"		Title="Last name" />
		<RadzenDataGridColumn TItem="Employee"	Property="Age"			Title="Age" />
	</Columns>
</RadzenDataGrid>

@code {
	public class Employee
	{
		public string? FirstName	{ get; set; }
		public string? LastName		{ get; set; }
		public int Age				{ get; set; }
	}

	private IEnumerable<Employee>? employees;
	private int totalCount;

	private DataGridSettings? _settings;
	private DataGridSettings? Settings
	{
		get {
			return _settings;
		}

		set {
			if(_settings != value)
			{
				_settings = value;
				InvokeAsync(SaveStateAsync);
			}
		}
	}

	private async Task LoadStateAsync()
	{
		await Task.CompletedTask;

		string result = await JSRuntime.InvokeAsync<string>("window.localStorage.getItem", "Settings");
		if(!string.IsNullOrEmpty(result))
		{
			_settings = JsonSerializer.Deserialize<DataGridSettings>(result);
			Console.WriteLine($"LoadStateAsync()");
		}
	}

	private async Task SaveStateAsync()
	{
		await Task.CompletedTask;

		await JSRuntime.InvokeVoidAsync("eval", $@"window.localStorage.setItem('Settings', '{JsonSerializer.Serialize<DataGridSettings>(Settings!)}')");
	}

	protected override async Task OnAfterRenderAsync(bool firstRender)
	{
		if(firstRender)
		{
			Console.WriteLine($"OnAfterRenderAsync(firstRender = {firstRender})");
			await LoadStateAsync();
			StateHasChanged();
		}
	}

	private void OnLoadData(LoadDataArgs args)
	{
		Console.WriteLine($"OnLoadDataEvent(LoadDataArgs args) / args.Filters.Count(): {args.Filters.Count()} / Settings?.Columns.Count(x => x.FilterValue is not null): {Settings?.Columns.Count(x => x.FilterValue is not null)}");

		IEnumerable<Employee> e = new List<Employee>() {
			new Employee { FirstName = "Joe",	LastName = "Green",   Age = 30 },
			new Employee { FirstName = "Joe",	LastName = "Blue",	     Age = 40 },
			new Employee { FirstName = "Jane",	LastName = "Green",   Age = 50 },
			new Employee { FirstName = "Jane",	LastName = "Blue",	     Age = 60 }
		};

		FilterDescriptor? firstNameFilter	= args.Filters.FirstOrDefault(x => x.Property == nameof(Employee.FirstName) && x.FilterValue is not null);
		FilterDescriptor? lastNameFilter	= args.Filters.FirstOrDefault(x => x.Property == nameof(Employee.LastName) && x.FilterValue is not null);
		FilterDescriptor? ageFilter			= args.Filters.FirstOrDefault(x => x.Property == nameof(Employee.Age) && x.FilterValue is not null);

		if(firstNameFilter is not null)
			e = e.Where(x => x.FirstName is not null && x.FirstName.Contains((string)firstNameFilter.FilterValue, StringComparison.InvariantCultureIgnoreCase));

		if(lastNameFilter is not null)
			e = e.Where(x => x.LastName is not null && x.LastName.Contains((string)lastNameFilter.FilterValue, StringComparison.InvariantCultureIgnoreCase));

		if(ageFilter is not null)
			e = e.Where(x => x.Age == (int)ageFilter.FilterValue);

		employees	= e.ToList();
		totalCount	= employees.Count();
	}
}

DataGrid will not be reloaded if the list is empty and later filled with data since since nothing will notify that this happened. Changing however the collection assigned to Data property from null to something will reload the DataGrid.

Ah, I see, back to the drawing board then...

Hello, I am experiencing a similar problem, the grid data loads correctly and the filters are also applied as expected. The problem comes when you make a filtering greater than (>) and save it in settings, when you load the configuration again it generates the error: operator '>' incompatible with operand types Decimal ans Double

@enchev another interesting debug session and i'm able to reproduce the "bug" by just making LoadData asynchronous... In my complete code example of the Index component where I've changed essentialy just this:

private void OnLoadData(LoadDataArgs args)
{
    ...

to

private async Task OnLoadData(LoadDataArgs args)
{
    await Task.Delay(1000);
    ...

My real component is using remote API to load data, so my LoadData method is async and awaits API call. I've hooked lifecycle methods to log them:

Synchronous LoadData:

OnInitialized()
OnParametersSet()
OnAfterRenderAsync(firstRender = True)
OnLoadDataEvent(LoadDataArgs args) / args.Filters.Count(): 0 / Settings?.Columns.Count(x => x.FilterValue is not null): 
OnAfterRenderAsync(firstRender = False)
LoadStateAsync()
OnAfterRenderAsync(firstRender = False)
OnLoadDataEvent(LoadDataArgs args) / args.Filters.Count(): 2 / Settings?.Columns.Count(x => x.FilterValue is not null): 2
OnAfterRenderAsync(firstRender = False)

Asynchronous LoadData with simulated roundtrip to API using Task.Delay:

OnInitialized()
OnParametersSet()
OnAfterRenderAsync(firstRender = True)
OnLoadDataEvent(LoadDataArgs args) / args.Filters.Count(): 0 / Settings?.Columns.Count(x => x.FilterValue is not null): 
OnAfterRenderAsync(firstRender = False)
LoadStateAsync()
OnAfterRenderAsync(firstRender = False)
OnAfterRenderAsync(firstRender = False)