Grid Filtering after State Restore Throws NullReferenceException
Environment
Product | Grid for Blazor, TreeList for Blazor |
Description
I see errors when I load the Grid state at initialization (OnStateInit
event handler) and then try to filter.
Steps to Reproduce
- Load (restore) the Grid state from a serialized Json string in the
OnStateInit
handler. - The serialized state should contain filter descriptors.
- Open a filter menu.
Error Message
The exception messages can vary, for example:
System.NullReferenceException: Object reference not set to an instance of an object.
at Telerik.Blazor.Common.Filter.FilterOperatorFactory.GetFilterOperatorsForType(Type type, ITelerikStringLocalizer localizer)
Or alternatively:
System.NullReferenceException: Object reference not set to an instance of an object.
at Telerik.Blazor.Components.Common.Filters.FilterList.TelerikFilterList.GetFilterOperators()
at Telerik.Blazor.Components.Common.Filters.FilterList.TelerikFilterList.InitFilterOperators()
If the Grid is bound to OData, the OData query may be incorrect and the following exception may occur:
A binary operator with incompatible types was detected. Found operand types '...' and '...' for operator kind '...'.", "type": "Microsoft.OData.ODataException".
Possible Cause
The FilterDescriptor
class has a MemberType
property that is of type Type
. The default JsonSerializer
is unable to serialize types. After state restore in OnStateInit
, the Grid filter descriptors end up with null
MemberType
values. This omission causes the errors.
There is a public issue about filtering error after Grid state restore in OnStateInit. Follow the item to receive status updates.
Solution
All suggested options are demonstrated in the examples below.
- Restore the Grid State later than
OnStateInit
. For example, useOnAfterRenderAsync
and the GridSetStateAsync()
method instead. - Manually set the missing
MemberType
property values in the restored filter descriptors inOnStateInit
. There is no need to include filter descriptors for all columns in the state object. - Restore the Grid State in a
try {} catch() {}
block. The GridOnStateInit
event fires two times - once in the prerender phase and once in the render phase. The deserialization problem will occur only if the Grid state is restored in the prerender phase. It is possible to skip state restoration during prerender with aJSInterop
call. This is not allowed during prerender, so it will trigger anInvalidOperationException
andOnStateInit
execution will abort. This approach is used in example Save and Load Grid State from Browser LocalStorage
There are serializers which support
Type
serialization, for example Newtonsoft Json.NET. They, however, can cause other undesired side effects.
Examples
@using System.Text.Json
<TelerikGrid @ref="@GridRef" />
@code {
private TelerikGrid<GridModel> GridRef { get; set; }
private string SerializedGridState { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var gridState = JsonSerializer.Deserialize<GridState<GridModel>>(SerializedGridState);
await GridRef.SetStateAsync(gridState);
}
await base.OnAfterRenderAsync(firstRender);
}
}
@using System.Text.Json
<TelerikGrid OnStateInit="@( (GridStateEventArgs<GridModel> args) => OnGridStateInit(args) )" />
@code {
private string SerializedGridState { get; set; }
private async Task OnStateInitHandler(GridStateEventArgs<GridModel> args)
{
var gridState = JsonSerializer.Deserialize<GridState<GridModel>>(SerializedGridState);
var itemType = typeof(GridModel);
foreach (CompositeFilterDescriptor cfd in gridState.FilterDescriptors)
{
foreach (FilterDescriptor fd in cfd.FilterDescriptors)
{
fd.MemberType = itemType.GetProperty(fd.Member).PropertyType;
}
}
args.GridState = gridState;
}
}
@using System.Text.Json
@inject IJSRuntime js
<TelerikGrid OnStateInit="@( (GridStateEventArgs<GridModel> args) => OnGridStateInit(args) )" />
<!-- suppress-error allows script tags in a .razor file. Avoid in production apps. -->
<script suppress-error="BL9992">function foo() {}</script>
@code {
private string SerializedGridState { get; set; }
private async Task OnGridStateInit(GridStateEventArgs<GridModel> args)
{
try
{
await js.InvokeVoidAsync("foo");
args.GridState = JsonSerializer.Deserialize<GridState<GridModel>>(SerializedGridState);
}
catch (InvalidOperationException e)
{
}
}
}