Duplicated
Last Updated: 19 Feb 2026 10:37 by Daniel
Daniel
Created on: 12 Feb 2026 11:51
Category: Grid
Type: Bug Report
1
TelerikGrid InitState and SearchFilter persistence error

I created a subclass of the TelerikGrid to extend the limited built-in search functionality.

To do that, I use the GridStateChanged event to rewrite the SearchFilter: I replace the default filter with a more complex CompositeFilterDescriptor (including highlighting). This works perfectly during normal interaction.

Now I also want the search to be persisted, so that after reloading the page the grid shows the same search again. Saving the grid state is not a problem, but when restoring the saved state during OnStateInit / GridStateInit, the following exception occurs:


Unable to cast object of type 'Telerik.DataSource.CompositeFilterDescriptor' to type 'Telerik.DataSource.FilterDescriptor'.
System.InvalidCastException: Unable to cast object of type 'Telerik.DataSource.CompositeFilterDescriptor' to type 'Telerik.DataSource.FilterDescriptor'.
at Telerik.Blazor.Components.Common.TableGridBase`2.LoadSearchFilter(IFilterDescriptor descriptor)
at Telerik.Blazor.Components.TelerikGrid`1.SetStateInternalAsync(GridState`1 state)
at Telerik.Blazor.Components.TelerikGrid`1.InvokeOnStateInit()
at Telerik.Blazor.Components.TelerikGrid`1.OnParametersSetAsync()
at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)
fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]
Unhandled exception in circuit 'MYsaCsOgfpbyHeO0xNFpA7ViPHNUC6rpc1K9eIwVR5Y'.
System.InvalidCastException: Unable to cast object of type 'Telerik.DataSource.CompositeFilterDescriptor' to type 'Telerik.DataSource.FilterDescriptor'.
at Telerik.Blazor.Components.Common.TableGridBase`2.LoadSearchFilter(IFilterDescriptor descriptor)
at Telerik.Blazor.Components.TelerikGrid`1.SetStateInternalAsync(GridState`1 state)
at Telerik.Blazor.Components.TelerikGrid`1.InvokeOnStateInit()
at Telerik.Blazor.Components.TelerikGrid`1.OnParametersSetAsync()
at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

I also tried saving the search term and the selected columns separately and then restoring the state in OnAfterRender, but at that point the SearchFilter can no longer be set.


How should I approach this? Is there a supported way to persist and restore a custom CompositeFilterDescriptor as the grid’s search filter (or otherwise restore the search state) without triggering this cast exception?

 

Sincerly Daniel

 

 

Duplicated
This item is a duplicate of an already existing item. You can find the original item here:
10 comments
Daniel
Posted on: 19 Feb 2026 10:37

I'll provide you an example tomorrow, don't have enough time today.

But yes it's an misunderstanding. It doesn't matter which custom FilterDescriptor you try to restore. It won't work.

The issue here is NOT the why how we build the Filter and how it looks like.

By the way no human is searching with a whole search string. Build an app and sell it, you will see ;) (tried Google?)

ADMIN
Dimo
Posted on: 19 Feb 2026 10:30

Hello Daniel,

If there is a misunderstanding, please provide an isolated runnable example for a review. For the time being, the scenario seems to boil down to searching by multiple words separately, which we have decided not to support. The built-in search functionality always uses the full string.

Regards,
Dimo
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.

Daniel
Posted on: 19 Feb 2026 09:56

What??

You can't close it as duplicated (and point to declined case) because it's not.

 

SetState with my own FilterCriteria is not working, doesn't matter how the criteria looks like.

 

 

ADMIN
Dimo
Posted on: 19 Feb 2026 09:51

Hi Daniel,

Thanks for the clarification. If I understand correctly, the app logic splits the user's search string by space and creates different filter descriptors for each separate word. This makes it non-deterministic for the Grid to go backwards and come up with the original search string from the saved CompositeFilterDescriptor. We start dealing with ambiguity and heuristics here, as there can be various different possible scenarios with AND and OR operators.

We already have a feature request about splitting the search string, so I am marking this one as a duplicate.

Regards,
Dimo
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.

Daniel
Posted on: 19 Feb 2026 09:23

In my application, the user searches for patients and opens one on a new page. When they navigate back, they expect to see the same search results so they can continue from where they left off. The search is important for certain batch tasks as well as for navigation. Otherwise, the user has to re-enter their search over and over again. I see this behavior handled properly in other applications, and it is very frustrating when it isn’t.

My filters are designed as an AND search (as a human would intuitively expect). For example, I search for “Joh Do” to find John Doe or Johanna Dolittle, even though first name and last name are stored in two separate columns.

Specifically, I construct the query as:

Col Firstname LIKE (Joh OR Doe) AND Col Lastname LIKE (Joh OR Doe)

ADMIN
Dimo
Posted on: 19 Feb 2026 09:08

Hi Daniel,

Please describe the following:

  • The use case scenario from user's perspective - what uses see in the UI and what actions do they perform.
  • How does the filter descriptor structure look like.

Regards,
Dimo
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.

Daniel
Posted on: 19 Feb 2026 09:00

Hi Dimo,

I’m trying to store the state in a separate object as a workaround. In doing so, I save the original filter as well as my token-based filter.

When initializing the state, I set the original filter and then attempt to apply my token-based filter in AfterRender.

Unfortunately, the error is still thrown even when setting it in AfterRender. I’m now wondering where I can apply my complex filter afterward without running into errors.

ADMIN
Dimo
Posted on: 19 Feb 2026 08:37

Hi all,

Currently the Grid expects the SearchFilter in the GridState to be a single CompositeFilterDescriptor with FilterDescriptor children. Assuming this structure, the Grid extracts the search string from the first child FilterDescriptor. If SearchFilter contains CompositeFilterDescriptor children, then there is an incorrect cast and the algorithm fails.

The current state of affairs is by design and surely not a bug. We can consider a feature request and make the Grid find the search string recursively in a collection of nested CompositeFilterDescriptors. But first, please describe your use cases in more detail, so that we can evaluate their applicability and potential for generalization. Keep in mind that even the most complex search criteria must be based on a single string value that the user can see or type in the built-in Grid search textbox.

Currently there are two possible workarounds:

@using Telerik.DataSource
@using System.Text.Json

<p>Define a search with any structure, but it must contain at least one <code>string</code> expression:</p>

<TelerikFilter Value="@FilterValue" OnUpdate="@(() => {})">
    <FilterFields>
        <FilterField Name="@nameof(Product.Name)" Type="@typeof(string)" />
        <FilterField Name="@nameof(Product.Description)" Type="@typeof(string)" />
        <FilterField Name="@nameof(Product.Group)" Type="@typeof(string)" />
        <FilterField Name="@nameof(Product.Price)" Type="@typeof(decimal)" />
        <FilterField Name="@nameof(Product.Quantity)" Type="@typeof(int)" />
        <FilterField Name="@nameof(Product.Released)" Type="@typeof(DateTime)" />
        <FilterField Name="@nameof(Product.Discontinued)" Type="@typeof(bool)" />
    </FilterFields>
</TelerikFilter>

<br />

<TelerikButton OnClick="@SetSearchFilter">Apply Search</TelerikButton>

<TelerikButton OnClick="@(() => ShouldRenderGrid = !ShouldRenderGrid)">Toggle Grid to call <code>OnStateInit</code></TelerikButton>

<br />
<br />

@if (ShouldRenderGrid)
{
    <TelerikGrid @ref="@GridRef"
                Data="@GridData"
                TItem="@Product"
                FilterMode="GridFilterMode.FilterRow"
                Pageable="true"
                Sortable="true"
                OnStateInit="@OnGridStateInit">
        <GridToolBar>
            <GridToolBarSearchBoxTool />
        </GridToolBar>
        <GridColumns>
            <GridColumn Field="@nameof(Product.Name)" />
            <GridColumn Field="@nameof(Product.Description)" />
            <GridColumn Field="@nameof(Product.Group)" />
            <GridColumn Field="@nameof(Product.Price)" DisplayFormat="{0:c2}" />
            <GridColumn Field="@nameof(Product.Quantity)" DisplayFormat="{0:n0}" />
            <GridColumn Field="@nameof(Product.Released)" DisplayFormat="{0:d}" />
            <GridColumn Field="@nameof(Product.Discontinued)" />
        </GridColumns>
    </TelerikGrid>
}

@code {
    private TelerikGrid<Product>? GridRef;
    private List<Product> GridData { get; set; } = new();

    private bool ShouldRenderGrid { get; set; } = true;

    private CompositeFilterDescriptor FilterValue { get; set; } = new();

    private async Task SetSearchFilter()
    {
        GridState<Product> gridState = GridRef!.GetState();

        ApplyFilterValueToGridState(gridState);

        await GridRef.SetStateAsync(gridState);
    }

    private void OnGridStateInit(GridStateEventArgs<Product> args)
    {
        if (FilterValue.FilterDescriptors.Count == 0)
        {
            return;
        }

        GridState<Product> gridState = args.GridState;

        ApplyFilterValueToGridState(gridState);
    }

    private void ApplyFilterValueToGridState(GridState<Product> gridState)
    {
        // Clone FilterValue to insert a dummy expression only for the Grid SearchState
        string searchFilterJson = JsonSerializer.Serialize(FilterValue);
        CompositeFilterDescriptor searchFilterObject = JsonSerializer.Deserialize<CompositeFilterDescriptor>(searchFilterJson)!;
        RestoreMemberTypeRecursively(searchFilterObject);

        IFilterDescriptor? firstFilterDesctiptor = searchFilterObject.FilterDescriptors.FirstOrDefault();

        if (firstFilterDesctiptor is null)
        {
            gridState.SearchFilter = null;
        }
        else if (firstFilterDesctiptor is FilterDescriptor fd && fd.MemberType == typeof(string))
        {
            gridState.SearchFilter = searchFilterObject;
        }
        else
        {
            string searchValue = FindSearchStringValueRecursively(searchFilterObject.FilterDescriptors);

            if (!string.IsNullOrEmpty(searchValue))
            {
                searchFilterObject.FilterDescriptors.Insert(0, new FilterDescriptor()
                {
                    Member = nameof(Product.Name),
                    Operator = FilterOperator.Contains,
                    Value = searchValue
                });

                gridState.SearchFilter = searchFilterObject;
            }
            else
            {
                gridState.SearchFilter = null;
            }
        }
    }

    private string FindSearchStringValueRecursively(FilterDescriptorCollection fdc)
    {
        foreach (IFilterDescriptor ifd in fdc)
        {
            if (ifd is FilterDescriptor fd)
            {
                if (fd.MemberType == typeof(string))
                {
                    return fd.Value.ToString() ?? string.Empty;
                }
                else
                {
                    continue;
                }
            }
            else
            {
                return FindSearchStringValueRecursively(((CompositeFilterDescriptor)ifd).FilterDescriptors);
            }
        }

        return string.Empty;
    }

    private void RestoreMemberTypeRecursively(CompositeFilterDescriptor cfd)
    {
        var itemType = typeof(Product);

        foreach (IFilterDescriptor ifd in cfd.FilterDescriptors)
        {
            if (ifd is FilterDescriptor fd)
            {
                fd.MemberType = itemType.GetProperty(fd.Member)!.PropertyType;
            }
            else if (ifd is CompositeFilterDescriptor childCfd)
            {
                RestoreMemberTypeRecursively(childCfd);
            }
        }
    }

    protected override void OnInitialized()
    {
        var rnd = Random.Shared;

        for (int i = 1; i <= 27; i++)
        {
            GridData.Add(new Product()
            {
                Id = i,
                Name = $"Name {i} {(char)rnd.Next(65, 91)}{(char)rnd.Next(65, 91)}",
                Group = $"Group {i % 3 + 1}",
                Price = rnd.Next(1, 100) * 1.23m,
                Quantity = rnd.Next(0, 10000),
                Released = DateTime.Today.AddDays(-rnd.Next(60, 1000)),
                Discontinued = i % 4 == 0
            });
        }
    }

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; } = string.Empty;
        public string Description { get; set; } = string.Empty;
        public string Group { get; set; } = string.Empty;
        public decimal Price { get; set; }
        public int Quantity { get; set; }
        public DateTime Released { get; set; }
        public bool Discontinued { get; set; }
    }
}

 

Regards,
Dimo
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.

Markus
Posted on: 18 Feb 2026 17:19

It seems that the problem occurs when the first IFilterDescriptor element in SearchFilter (type CompositeFilterDescriptor) itself is of type CompositeFilterDescriptor. In my case, it helped to either change the order of the IFilterDescriptor elements in SearchFilter so that an element of type FilterDescriptor is in first place, or to create a new fake/placeholder FilterDescriptor that is placed in first place in SearchFilter. Before adopting it, however, you need to check whether it suits your requirements. There is no guarantee that it will work for you:

private async Task SetSearchFilterAsync(CompositeFilterDescriptor compositeFilterDescriptor = null)
{
    var firstDescriptor = compositeFilterDescriptor?.FilterDescriptors.FirstOrDefault();
    //Check if first IFilterDescriptor in CompositeFilterDescriptor (for SearchFilter) is from Type CompositeFilterDescriptor
    if(firstDescriptor != null && firstDescriptor is CompositeFilterDescriptor)
    {
        //Check occurence of FilterDescriptor in CompositeFilterDescriptor -> Swap Order so that first IFilterDescriptor is from Type FilterDescriptor 
        var firstNonCompositeFilterDescriptor = compositeFilterDescriptor?.FilterDescriptors.FirstOrDefault(fd => fd is FilterDescriptor);
        if (firstNonCompositeFilterDescriptor != null)
        {
            compositeFilterDescriptor.FilterDescriptors.Remove(firstNonCompositeFilterDescriptor);
            compositeFilterDescriptor.FilterDescriptors.Insert(0, firstNonCompositeFilterDescriptor);
        }
        else
        {
            //In Case there is no FilterDescriptor-Item in CompositeFilterDescriptor: Generate a placeholder-FilterDescriptor from CompositeFilterOperators
            //and insert it as first item of CompositeFilterOperator
            IFilterDescriptor firstChildDescriptor;
            do
            {
                firstChildDescriptor = (firstDescriptor as CompositeFilterDescriptor).FilterDescriptors.First();
            }
            while (firstChildDescriptor is not FilterDescriptor);
            var childDescriptor = firstChildDescriptor as FilterDescriptor;
            var placeholderDescriptor = new FilterDescriptor(childDescriptor.Member, FilterOperator.IsEqualTo, childDescriptor.Value);
            placeholderDescriptor.MemberType = childDescriptor.MemberType;
            compositeFilterDescriptor.FilterDescriptors.Insert(0, childDescriptor);
        }
    }
    await OnSearch.InvokeAsync(compositeFilterDescriptor); //--> will be assigned to SearchFilter in another Component
}
Markus
Posted on: 18 Feb 2026 16:02

Hello Telerik, Hello Daniel,

Today I encountered the same problem.

Sincerly Markus