Unplanned
Last Updated: 08 Feb 2021 09:48 by ADMIN
qw
Created on: 03 Feb 2021 10:45
Category: Grid
Type: Bug Report
6
Set deserialized grid state in OnStateInit handler cause error on open filter menu of column on UI

Error:

blazor.server.js:19 [2021-02-03T06:17:43.996Z] Error: 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()
   at Telerik.Blazor.Components.Common.Filters.FilterList.TelerikFilterList.OnInitializedAsync()
   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
e.log @ blazor.server.js:19
C @ blazor.server.js:8
(anonymous) @ blazor.server.js:8
(anonymous) @ blazor.server.js:1
e.invokeClientMethod @ blazor.server.js:1
e.processIncomingData @ blazor.server.js:1
connection.onreceive @ blazor.server.js:1
i.onmessage @ blazor.server.js:1

 

Example:

private string SerializedState; private void OnStateInitHandler(GridStateEventArgs<SampleData> args) { args.GridState = JsonSerializer.Deserialize<GridState<SampleData>>(SerializedState); }

 

Reason:

FilterDescriptors property MemberType = null.

 

Note:

Method TelerikGrid.SetState() works correctly.

 

 

 

5 comments
ADMIN
Marin Bratanov
Posted on: 08 Feb 2021 09:48

Indeed, that's a valid approach for a workaround. I just thought that using OnAfterRenderAsync is a bit easier and requires less code.

 

Regards,
Marin Bratanov
Progress Telerik

Virtual Classroom, the free self-paced technical training that gets you up to speed with Telerik and Kendo UI products quickly just got a fresh new look + new and improved content including a brand new Blazor course! Check it out at https://learn.telerik.com/.

qw
Posted on: 08 Feb 2021 09:37
Hello Marin,

Thank you for the answer.

Unfortunately, I have specific case and suggested workarounds did not work in it. But I found another solution - just set MemberType manually.

@page "/"
@using System.Text.Json

<TelerikButton OnClick="SaveState">Save state</TelerikButton>
<TelerikButton OnClick="LoadState">Load state</TelerikButton>

<TelerikGrid @ref="Grid"
             Data="Data"
             OnStateInit="OnGridStateInit"
             FilterMode="GridFilterMode.FilterMenu"
             TItem="SampleData">
    <GridColumns>
        <GridColumn Title="Name" Field="@nameof(SampleData.Name)" />
    </GridColumns>
</TelerikGrid>

@code {

    private TelerikGrid<SampleData> Grid;
    private IEnumerable<SampleData> Data;
    private string SerializedState;

    protected override void OnInitialized()
    {
        // load serialized state from DB, for example
        SerializedState = @"
{
  ""GroupDescriptors"": [],
  ""FilterDescriptors"": [
    {
      ""LogicalOperator"": 0,
      ""FilterDescriptors"": [
        {
          ""Member"": ""Name"",
          ""Operator"": 8,
          ""Value"": null
        },
        {
          ""Member"": ""Name"",
          ""Operator"": 8,
          ""Value"": null
        }
      ]
    }
  ],
  ""SortDescriptors"": [],
  ""Page"": 1,
  ""Skip"": 0,
  ""CollapsedGroups"": [],
  ""ColumnStates"": [
    {
      ""Index"": 0,
      ""Width"": null,
      ""Visible"": null,
      ""Locked"": false
    }
  ],
  ""ExpandedRows"": [],
  ""SelectedItems"": [],
  ""OriginalEditItem"": null,
  ""EditItem"": null,
  ""EditField"": null,
  ""InsertedItem"": null,
  ""TableWidth"": null
}";

        Data = Enumerable.Empty<SampleData>();
    }

    private void OnGridStateInit(GridStateEventArgs<SampleData> args)
    {
var state = JsonSerializer.Deserialize<GridState<SampleData>>(SerializedState);
var itemType = typeof(SampleData);
state.FilterDescriptors
.OfType<CompositeFilterDescriptor>()
.SelectMany(f => f.FilterDescriptors.OfType<FilterDescriptor>())
.Concat(state.FilterDescriptors.OfType<FilterDescriptor>())
.GroupBy(f => f.Member)
.ForEach(g =>
{
var memberType = itemType.GetProperty(g.Key).PropertyType;
g.ForEach(f => f.MemberType = memberType);
});

        args.GridState = state;
    }

    private void SaveState()
    {
        var state = Grid.GetState();
        SerializedState = JsonSerializer.Serialize(state, new JsonSerializerOptions { WriteIndented = true });

        // save serialized state to DB, for example
    }

    private void LoadState()
    {
        var state = JsonSerializer.Deserialize<GridState<SampleData>>(SerializedState);
        Grid.SetState(state);
    }

    private class SampleData
    {
        public string Name { get; set; }
    }
}
ADMIN
Marin Bratanov
Posted on: 04 Feb 2021 17:50

Hello,

Thank you for the details.

Here is what happens:

  • Properties of type Type cannot be serialized (limitation of the framework), so after a state has been serialized they will be lost.
  • Without this property, the filter list cannot be initialized properly - the list of available operators depends on the type. Hence, the error.
  • The StateInit fires early in the component lifecycle so it is before data requests, but this also means that it fires before child components are initialized. This is of interest, because the columns are not available yet, so they can't help by providing the type of the field they are bound to

So, the grid should not throw this exception and, ideally, the filter should keep working in this scenario.

That said, I can offer two workarounds - the first is a tad more generic, the second might help for this particular case so that you can avoid loading state unless it is necessary.

Idea 1: Load the state in the AfterRender event. Caveat - there may be a second data request if you use the OnRead event.

 

@using System.Text.Json

<TelerikButton OnClick="SaveState">Save state</TelerikButton>
<TelerikButton OnClick="LoadState">Load state</TelerikButton>
<TelerikButton OnClick="SetCustomState">Set Custom state</TelerikButton>

<TelerikGrid @ref="Grid"
             Data="Data"
             FilterMode="GridFilterMode.FilterMenu"
             TItem="SampleData">
    <GridColumns>
        <GridColumn Title="Name" Field="@nameof(SampleData.Name)" />
    </GridColumns>
</TelerikGrid>

@SerializedState

@code {

    private TelerikGrid<SampleData> Grid;
    private IEnumerable<SampleData> Data;
    private string SerializedState;

    protected override void OnInitialized()
    {
        
        if (string.IsNullOrEmpty(SerializedState))
        {
            SerializedState = @"
{
  ""GroupDescriptors"": [],
  ""FilterDescriptors"": [
    {
      ""LogicalOperator"": 0,
      ""FilterDescriptors"": [
        {
          ""Member"": ""Name"",
          ""Operator"": 8,
          ""Value"": null
        },
        {
          ""Member"": ""Name"",
          ""Operator"": 8,
          ""Value"": null
        }
      ]
    }
  ],
  ""SortDescriptors"": [],
  ""Page"": 1,
  ""Skip"": 0,
  ""CollapsedGroups"": [],
  ""ColumnStates"": [
    {
      ""Index"": 0,
      ""Width"": null,
      ""Visible"": null,
      ""Locked"": false
    }
  ],
  ""ExpandedRows"": [],
  ""SelectedItems"": [],
  ""OriginalEditItem"": null,
  ""EditItem"": null,
  ""EditField"": null,
  ""InsertedItem"": null,
  ""TableWidth"": null
}";
        }
        Data = Enumerable.Empty<SampleData>();
        //Data = Enumerable.Range(1, 15).Select(x => new SampleData { Name = $"name {x}" }).ToList();
    }

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

    private async void SaveState()
    {
        var state = Grid.GetState();

        SerializedState = JsonSerializer.Serialize(state);

    }

    private async Task LoadState()
    {
        if (!string.IsNullOrEmpty(SerializedState))
        {
            var state = JsonSerializer.Deserialize<GridState<SampleData>>(SerializedState);
            if (state != null)
            {
                await Grid.SetState(state);
            }
        }
    }

    private async Task SetCustomState()
    {
        string customSerializedState = @"{""roupDescriptors"":[],""FilterDescriptors"":[{""LogicalOperator"":0,""FilterDescriptors"":[{""Member"":""Name"",""Operator"":8,""Value"":""1""},{""Member"":""Name"",""Operator"":8,""Value"":""2""}]}],""SortDescriptors"":[],""Page"":1,""Skip"":0,""CollapsedGroups"":[],""ColumnStates"":[{""Index"":0,""Width"":null,""Visible"":null,""Locked"":false}],""ExpandedRows"":[],""SelectedItems"":[],""OriginalEditItem"":null,""EditItem"":null,""EditField"":"""",""InsertedItem"":null,""TableWidth"":null}";

        SerializedState = customSerializedState;

        var state = JsonSerializer.Deserialize<GridState<SampleData>>(customSerializedState);
        if (state != null)
        {
            await Grid.SetState(state);
        }
    }

    private class SampleData
    {
        public int Id { get; set; }
        public string Name { get; set; }

        // example of comparing stored items (from editing or selection)
        // with items from the current data source - IDs are used instead of the default references
        public override bool Equals(object obj)
        {
            if (obj is SampleData)
            {
                return this.Id == (obj as SampleData).Id;
            }
            return false;
        }
    }
}

 

Idea 2: Try to load state into the grid when it actually makes sense to load new information. The provided state object is, effectively, empty, and you could avoid saving that in the first place. If you want to save the state through an explicit user action such as a button click, you can at least raise a flag in the OnStateChanged event to store only a state that has changed. Here is an example of that:

 

@using System.Text.Json

<TelerikButton OnClick="SaveState">Save state</TelerikButton>
<TelerikButton OnClick="LoadState">Load state</TelerikButton>
<TelerikButton OnClick="SetCustomState">Set Custom state</TelerikButton>
<br />State has changed so it makse sense to save it: @HasStateChanged
<TelerikGrid @ref="Grid"
             Data="Data"
             OnStateInit="@((GridStateEventArgs<SampleData> args) => OnGridStateInit(args))"
             OnStateChanged="@((GridStateEventArgs<SampleData> args) => HasStateChanged = true)"
             FilterMode="GridFilterMode.FilterMenu"
             TItem="SampleData">
    <GridColumns>
        <GridColumn Title="Name" Field="@nameof(SampleData.Name)" />
    </GridColumns>
</TelerikGrid>

@SerializedState

@code {
    string UniqueStorageKey = "TestGridKey";
    private TelerikGrid<SampleData> Grid;
    private IEnumerable<SampleData> Data;
    bool HasStateChanged { get; set; }
    private string SerializedState; // this is null when there is no state to actually save - see the bool flag above it

    protected override void OnInitialized()
    {
        Data = Enumerable.Empty<SampleData>();
        //Data = Enumerable.Range(1, 15).Select(x => new SampleData { Name = $"name {x}" }).ToList();
    }

    private async Task OnGridStateInit(GridStateEventArgs<SampleData> args)
    {
        if (!string.IsNullOrEmpty(SerializedState))
        {
            var state = JsonSerializer.Deserialize<GridState<SampleData>>(SerializedState);
            if (state != null)
            {
                args.GridState = state;
            }
        }
    }

    private async void SaveState()
    {
        if (HasStateChanged)
        {
            var state = Grid.GetState();
            SerializedState = JsonSerializer.Serialize(state);
        }
    }

    private async Task LoadState()
    {
        if (!string.IsNullOrEmpty(SerializedState))
        {
            var state = JsonSerializer.Deserialize<GridState<SampleData>>(SerializedState);
            if (state != null)
            {
                await Grid.SetState(state);
            }
        }
    }

    private async Task SetCustomState()
    {
        string customSerializedState = @"{""roupDescriptors"":[],""FilterDescriptors"":[{""LogicalOperator"":0,""FilterDescriptors"":[{""Member"":""Name"",""Operator"":8,""Value"":""1""},{""Member"":""Name"",""Operator"":8,""Value"":""2""}]}],""SortDescriptors"":[],""Page"":1,""Skip"":0,""CollapsedGroups"":[],""ColumnStates"":[{""Index"":0,""Width"":null,""Visible"":null,""Locked"":false}],""ExpandedRows"":[],""SelectedItems"":[],""OriginalEditItem"":null,""EditItem"":null,""EditField"":"""",""InsertedItem"":null,""TableWidth"":null}";

        var state = JsonSerializer.Deserialize<GridState<SampleData>>(customSerializedState);
        if (state != null)
        {
            await Grid.SetState(state);
        }
    }

    private class SampleData
    {
        public int Id { get; set; }
        public string Name { get; set; }

        // example of comparing stored items (from editing or selection)
        // with items from the current data source - IDs are used instead of the default references
        public override bool Equals(object obj)
        {
            if (obj is SampleData)
            {
                return this.Id == (obj as SampleData).Id;
            }
            return false;
        }
    }
}

 

 

Regards,
Marin Bratanov
Progress Telerik

Virtual Classroom, the free self-paced technical training that gets you up to speed with Telerik and Kendo UI products quickly just got a fresh new look + new and improved content including a brand new Blazor course! Check it out at https://learn.telerik.com/.

qw
Posted on: 04 Feb 2021 05:43

Hello,

Clarify:

  • the state gets from grid (method GetState()) then serializes to json, the MemberType is not serializable, it absent in json-string, however in state object it setted
  • the .NET seializer is used (System.Text.Json)
  • library version 2.20, .NET 5, Blazor server side

 

Extended example:

@page "/"
@using System.Text.Json

<TelerikButton OnClick="SaveState">Save state</TelerikButton>
<TelerikButton OnClick="LoadState">Load state</TelerikButton>

<TelerikGrid @ref="Grid"
             Data="Data"
             OnStateInit="OnGridStateInit"
             FilterMode="GridFilterMode.FilterMenu"
             TItem="SampleData">
    <GridColumns>
        <GridColumn Title="Name" Field="@nameof(SampleData.Name)" />
    </GridColumns>
</TelerikGrid>

@code {

    private TelerikGrid<SampleData> Grid;
    private IEnumerable<SampleData> Data;
    private string SerializedState;

    protected override void OnInitialized()
    {
        // load serialized state from DB, for example
        SerializedState = @"
{
  ""GroupDescriptors"": [],
  ""FilterDescriptors"": [
    {
      ""LogicalOperator"": 0,
      ""FilterDescriptors"": [
        {
          ""Member"": ""Name"",
          ""Operator"": 8,
          ""Value"": null
        },
        {
          ""Member"": ""Name"",
          ""Operator"": 8,
          ""Value"": null
        }
      ]
    }
  ],
  ""SortDescriptors"": [],
  ""Page"": 1,
  ""Skip"": 0,
  ""CollapsedGroups"": [],
  ""ColumnStates"": [
    {
      ""Index"": 0,
      ""Width"": null,
      ""Visible"": null,
      ""Locked"": false
    }
  ],
  ""ExpandedRows"": [],
  ""SelectedItems"": [],
  ""OriginalEditItem"": null,
  ""EditItem"": null,
  ""EditField"": null,
  ""InsertedItem"": null,
  ""TableWidth"": null
}";

        Data = Enumerable.Empty<SampleData>();
    }

    private void OnGridStateInit(GridStateEventArgs<SampleData> args)
    {
        args.GridState = JsonSerializer.Deserialize<GridState<SampleData>>(SerializedState);
    }

    private void SaveState()
    {
        var state = Grid.GetState();
        SerializedState = JsonSerializer.Serialize(state, new JsonSerializerOptions { WriteIndented = true });

        // save serialized state to DB, for example
    }

    private void LoadState()
    {
        var state = JsonSerializer.Deserialize<GridState<SampleData>>(SerializedState);
        Grid.SetState(state);
    }

    private class SampleData
    {
        public string Name { get; set; }
    }
}

 

Reproduction steps:

  • Error case
    1. Open page
    2. Click on filter menu of column

     

  • Working case
    1. Open page
    2. Click on button "LoadState"
    3. Click on filter menu of column

 

 

 

ADMIN
Marin Bratanov
Posted on: 03 Feb 2021 17:28

Hello,

Can you confirm that:

  • the MemberType is set when saving the state (especially if the state is generated by the application code, not by the grid)
  • the .NET seializer is used (System.Text.Json) rather than a custom serializer such as Newtonsoft.Json 

If you are seeing a problem with the built-in supported setup (filter generated by the grid erroneously, or the System.Text.Json serializer failing) with the latest version (2.21.1 at the moment), please modify the sample from the documentation to showcase it: https://docs.telerik.com/blazor-ui/components/grid/state#save-and-load-grid-state-from-browser-localstorage so I can have a look.

 

Regards,
Marin Bratanov
Progress Telerik

Virtual Classroom, the free self-paced technical training that gets you up to speed with Telerik and Kendo UI products quickly just got a fresh new look + new and improved content including a brand new Blazor course! Check it out at https://learn.telerik.com/.