Grid State
The Grid lets you save, load and change its current state through code. The state management includes all the user-configurable elements of the grid - such as sorting, filtering, paging, grouping, edited items, selection, column size and order.
You can see this feature in the Live Demo: Grid State.
This article contains the following sections:
- Basics
- Information in the Grid State
-
Examples
- Save and Load Grid State from Browser LocalStorage
- Save and Load Grid State in a WebAssembly application
- Set Grid Options Through State
- Set Default (Initial) State
- Get and Override User Action That Changes The Grid
- Initiate Editing or Inserting of an Item
- Get Current Columns Visibility, Order, Field
Basics
The grid state is a generic class whose type is determined by the type of the model you use for the grid. It contains fields that correspond to the grid behaviors which you can use to save, load and modify the grid state.
Fields that pertain to model data (such as edited item, inserted item, selected items) are also typed according to the grid model. If you restore such data, make sure to implement appropriate comparison checks - by default the .Equals
check for a class (model) is a reference check and the reference from the storage is unlikely to match the reference from the grid Data
. Thus, you may want to override the .Equals
method of the model you use so it compares by an ID, for example, or otherwise (in the app logic) re-populate the models in the state object with the new model references from the grid data source.
The grid offers two events and two methods to allow flexible operations over its state:
Events
The OnStateInit
and OnStateChanged
events are raised by the grid so you can have an easy to use hook for loading and saving state, respectively.
OnStateInit
fires when the grid is initializing and you can provide the state you load from your storage to theGridState
field of its event arguments.-
OnStateChanged
fires when the user makes a change to the grid state (such as paging, sorting, grouping, filtering, editing, selecting and so on). TheGridState
field of the event argument provides the current grid state so you can store it. ThePropertyName
field of the event arguments indicates what is the aspect that changed.- The possible values for the
PropertyName
areSortDescriptors
,FilterDescriptors
,SearchFilter
,GroupDescriptors
,Page
,Skip
,CollapsedGroups
,ColumnStates
,ExpandedItems
,InsertedItem
,OriginalEditItem
,EditItem
. - We recommend that you use an
async void
handler for theOnStateChanged
event in order to reduce re-rendering and to avoid blocking the UI update while waiting for the service to store the data. Doing so will let the UI thread continue without waiting for the storage service to complete. In case you need to execute logic that requires UI update, useasync Task
. - Filtering always resets the current page to 1, so the
OnStateChanged
event will fire twice. First,PropertyName
will be equal to"Page"
, and the second time it will be"FilterDescriptors"
. However, theGridState
field of the event argument will provide correct information about the overall Grid state in both event handler executions.
- The possible values for the
By using the OnStateChanged
and OnStateInit
events, you can save and restore the grid layout for your users by calling your storage service in the respective handler.
Methods
The GetState
and SetStateAsync
instance methods provide flexibility for your business logic. They let you get and set the current grid state on demand outside of the grid events.
GetState
returns the grid state so you can store it only on a certain condition - for example, you may want to save the grid layout only on a button click, and not on every user interaction with the grid. You can also use it to get information about the current state of the filters, sorts and so on, if you are not using the OnRead event.SetStateAsync
takes an instance of a grid state so you can use your own code to alter the grid layout and state. For example, you can have a button that puts the grid in a certain configuration that helps your users review data (like certain filters, sorts, groups, expanded detail templates, initiate item editing or inserting, etc.).
If you want to make changes on the current grid state, first get it from the grid through the GetState
method, then apply the modifications on the object you got and pass it to SetStateAsync
.
If you want to put the grid in a certain configuration without preserving the old one, create a new GridState<T>()
and apply the settings there, then pass it to SetStateAsync
.
To reset the grid state, call SetStateAsync(null)
.
You should avoid calling SetStateAsync
in the grid CRUD methods (such as OnRead, OnUpdate
, OnEdit
, OnCreate
, OnCancel
). Doing so may lead to unexpected results because the grid has more logic to execute after the event.
Information in the Grid State
The following information is present in the grid state:
Editing - whether the user was inserting or editing an item (opens the same item for editing with the current data from the built-in editors of the grid - the data is updated in the
OnChange
event, not on every keystroke for performance reasons). TheOriginalEditItem
carries the original model without the user modifications so you can compare.Filtering - filter descriptors (fields by which the grid is filtered, the operator and value).
SearchFilter - filter descriptor specific to the GridSearchBox.
Grouping - group descriptors (fields by which the grid is grouped), collapsed group indexes.
Paging - page index, offset (skip) for virtual scrolling.
Rows - list of expanded items.
Sorting - sort descriptors (fields by which the grid is sorted, and the direction).
Selection - list of selected items.
-
Columns - Visible, Width, Index (order) of the column that the user sees, Locked (pinned).
-
The grid matches the columns from its markup sequentially (in the same order) with the columns list in the state object. So, when you restore/set the state, the grid must initialize with the same collection of columns that were used to save the state.
The
Index
field in the column state object represents its place (order) that the user sees and can choose through theReordable
feature, not its place in the grid markup. You can find an example below.If you want to change the visibility of columns, we recommend you use their
Visible
parameter rather than conditional markup - this parameter will be present in the state and will not change the columns collection count which makes it easier to reconcile changes.
-
Examples
You can find the following examples in this section:
- Save and Load Grid State from Browser LocalStorage
- Set Grid Options Through State
- Set Default (Initial) State
- Get and Override User Action That Changes The Grid
- Initiate Editing or Inserting of an Item
- Get Current Columns Visibility, Order, Field
Save and Load Grid State from Browser LocalStorage
The following example shows one way you can store the grid state - through a custom service that calls the browser's LocalStorage. You can use your own database here, or a file, or Microsoft's ProtectedBrowserStorage package, or any other storage you prefer. This is just an example you can use as base and modify to suit your project.
We support the
System.Text.Json
serialization that is built-in in Blazor. Be aware of its limitation to not serializeType
properties.
Save, Load, Reset grid state on every state change. Uses a sample LocalStorage in the browser.
@inject LocalStorage LocalStorage
@inject IJSRuntime JsInterop
Change something in the grid (like sort, filter, select, page, resize columns, etc.), then reload the page to see the grid state fetched from the browser local storage.
<br />
<TelerikButton OnClick="@ReloadPage">Reload the page to see the current grid state preserved</TelerikButton>
<TelerikButton OnClick="@ResetState">Reset the state</TelerikButton>
<TelerikGrid Data="@GridData" Height="500px" @ref="@Grid"
Groupable="true"
Pageable="true"
Sortable="true"
FilterMode="@GridFilterMode.FilterRow"
Reorderable="true"
Resizable="true"
SelectionMode="GridSelectionMode.Multiple" @bind-SelectedItems="@SelectedItems"
OnUpdate=@UpdateItem OnDelete=@DeleteItem OnCreate=@CreateItem EditMode="@GridEditMode.Inline"
OnStateInit="@((GridStateEventArgs<SampleData> args) => OnStateInitHandler(args))"
OnStateChanged="@((GridStateEventArgs<SampleData> args) => OnStateChangedHandler(args))">
<DetailTemplate>
@{
var employee = context as SampleData;
<TelerikGrid Data="employee.Assignments" Pageable="true" PageSize="5">
<GridColumns>
<GridColumn Field="AssignmentId" Title="Assignment Id"></GridColumn>
<GridColumn Field="AssignmentTitle" Title="Assignment Title"></GridColumn>
</GridColumns>
</TelerikGrid>
}
</DetailTemplate>
<GridColumns>
<GridColumn Field="@(nameof(SampleData.Id))" Editable="false" />
<GridColumn Field="@(nameof(SampleData.Name))" Title="Employee Name" />
<GridColumn Field="@(nameof(SampleData.Team))" Title="Team" />
<GridCommandColumn>
<GridCommandButton Command="Edit" Icon="@FontIcon.Pencil">Edit</GridCommandButton>
<GridCommandButton Command="Delete" Icon="@FontIcon.Trash">Delete</GridCommandButton>
<GridCommandButton Command="Save" Icon="@FontIcon.Save" ShowInEdit="true">Save</GridCommandButton>
<GridCommandButton Command="Cancel" Icon="@FontIcon.Cancel" ShowInEdit="true">Cancel</GridCommandButton>
</GridCommandColumn>
</GridColumns>
<GridToolBarTemplate>
<GridCommandButton Command="Add" Icon="@FontIcon.Plus">Add Employee</GridCommandButton>
</GridToolBarTemplate>
</TelerikGrid>
@if (SelectedItems != null)
{
<ul>
@foreach (SampleData employee in SelectedItems)
{
<li>
@employee.Id
</li>
}
</ul>
}
@code {
// Load and Save the state through the grid events
string UniqueStorageKey = "SampleGridStateStorageThatShouldBeUnique";
TelerikGrid<SampleData> Grid { get; set; }
IEnumerable<SampleData> SelectedItems { get; set; } = Enumerable.Empty<SampleData>();
List<SampleData> GridData { get; set; }
async Task OnStateInitHandler(GridStateEventArgs<SampleData> args)
{
try
{
var state = await LocalStorage.GetItem<GridState<SampleData>>(UniqueStorageKey);
if (state != null)
{
args.GridState = state;
}
}
catch (InvalidOperationException e)
{
// the JS Interop for the local storage cannot be used during pre-rendering
// so the code above will throw. Once the app initializes, it will work fine
}
}
async void OnStateChangedHandler(GridStateEventArgs<SampleData> args)
{
await LocalStorage.SetItem(UniqueStorageKey, args.GridState);
}
async Task ResetState()
{
// clean up the storage
await LocalStorage.RemoveItem(UniqueStorageKey);
await Grid.SetStateAsync(null); // pass null to reset the state
}
void ReloadPage()
{
JsInterop.InvokeVoidAsync("window.location.reload");
}
// Sample CRUD operations
async Task UpdateItem(GridCommandEventArgs args)
{
SampleData item = (SampleData)args.Item;
// perform actual data source operations here through your service
await MyService.Update(item);
// update the local view-model data with the service data
await GetGridData();
Console.WriteLine("Update event is fired.");
}
async Task DeleteItem(GridCommandEventArgs args)
{
SampleData item = (SampleData)args.Item;
// perform actual data source operation here through your service
await MyService.Delete(item);
// update the local view-model data with the service data
await GetGridData();
Console.WriteLine("Delete event is fired.");
}
async Task CreateItem(GridCommandEventArgs args)
{
SampleData item = (SampleData)args.Item;
// perform actual data source operation here through your service
await MyService.Create(item);
// update the local view-model data with the service data
await GetGridData();
Console.WriteLine("Create event is fired.");
}
// Note the Equals override for restoring selection and editing
public class SampleData
{
public int Id { get; set; }
public string Name { get; set; }
public string Team { get; set; }
public List<Assignment> Assignments { 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;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
}
public class Assignment
{
public int AssignmentId { get; set; }
public string AssignmentTitle { get; set; }
}
async Task GetGridData()
{
GridData = await MyService.Read();
}
protected override async Task OnInitializedAsync()
{
await GetGridData();
}
// the following static class mimics an actual data service that handles the actual data source
// replace it with your actual service through the DI, this only mimics how the API can look like and works for this standalone page
public static class MyService
{
private static List<SampleData> _data { get; set; } = new List<SampleData>();
public static async Task Create(SampleData itemToInsert)
{
itemToInsert.Id = _data.Count + 1;
_data.Insert(0, itemToInsert);
}
public static async Task<List<SampleData>> Read()
{
if (_data.Count < 1)
{
for (int i = 1; i < 50; i++)
{
SampleData employee = new SampleData { Id = i, Name = $"Name {i}", Team = "team " + i % 5 };
employee.Assignments = Enumerable.Range(1, 15).Select(x => new Assignment { AssignmentId = x, AssignmentTitle = "Assignment " + x }).ToList();
_data.Add(employee);
}
}
return await Task.FromResult(_data);
}
public static async Task Update(SampleData itemToUpdate)
{
var index = _data.FindIndex(i => i.Id == itemToUpdate.Id);
if (index != -1)
{
_data[index] = itemToUpdate;
}
}
public static async Task Delete(SampleData itemToDelete)
{
_data.Remove(itemToDelete);
}
}
}
using Microsoft.JSInterop;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Telerik.DataSource;
public class LocalStorage
{
protected IJSRuntime JSRuntimeInstance { get; set; }
public LocalStorage(IJSRuntime jsRuntime)
{
JSRuntimeInstance = jsRuntime;
}
public ValueTask SetItem(string key, object data)
{
return JSRuntimeInstance.InvokeVoidAsync(
"localStorage.setItem",
new object[] {
key,
JsonSerializer.Serialize(data)
});
}
public async Task<T> GetItem<T>(string key)
{
var data = await JSRuntimeInstance.InvokeAsync<string>("localStorage.getItem", key);
if (!string.IsNullOrEmpty(data))
{
return JsonSerializer.Deserialize<T>(data);
}
return default;
}
public ValueTask RemoveItem(string key)
{
return JSRuntimeInstance.InvokeVoidAsync("localStorage.removeItem", key);
}
}
Save and Load Grid State in a WebAssembly application
The knowledge base article for saving the Grid state in a WASM application explains two ways of storing the Grid
state - through a custom controller and a custom service that calls the browser's LocalStorage.
Set Grid Options Through State
The grid state allows you to control the behavior of the grid programmatically - you can, for example, set sorts, filteres, expand hierarhical rows, collapse groups.
The individual tabs below show how you can use the state to programmatically set the grid filtering, sorting, grouping and other features.
If you want to set an initial state to the grid, use a similar snippet, but in the
OnStateInit event
@* This snippet shows how to set sorting state to the grid from your code *@
@using Telerik.DataSource;
<TelerikButton ThemeColor="primary" OnClick="@SetGridSort">set sort from code</TelerikButton>
<TelerikGrid Data="@MyData" Height="400px" @ref="@Grid"
Pageable="true" Sortable="true">
<GridColumns>
<GridColumn Field="@(nameof(SampleData.Id))" Width="120px" />
<GridColumn Field="@(nameof(SampleData.Name))" Title="Employee Name" />
<GridColumn Field="@(nameof(SampleData.Team))" Title="Team" />
<GridColumn Field="@(nameof(SampleData.HireDate))" Title="Hire Date" />
</GridColumns>
</TelerikGrid>
@code {
public TelerikGrid<SampleData> Grid { get; set; }
async Task SetGridSort()
{
GridState<SampleData> desiredState = new GridState<SampleData>()
{
SortDescriptors = new List<SortDescriptor>()
{
new SortDescriptor { Member = "Id", SortDirection = ListSortDirection.Descending }
}
};
await Grid.SetStateAsync(desiredState);
}
public IEnumerable<SampleData> MyData = Enumerable.Range(1, 30).Select(x => new SampleData
{
Id = x,
Name = "name " + x,
Team = "team " + x % 5,
HireDate = DateTime.Now.AddDays(-x).Date
});
public class SampleData
{
public int Id { get; set; }
public string Name { get; set; }
public string Team { get; set; }
public DateTime HireDate { get; set; }
}
}
@* This snippet shows how to set filtering state to the grid from your code
Applies to the FilterRow mode *@
@using Telerik.DataSource;
<TelerikButton ThemeColor="primary" OnClick="@SetGridFilter">Filter From Code</TelerikButton>
<TelerikGrid @ref="@GridRef"
Data="@GridData"
Height="400px"
Pageable="true"
FilterMode="@GridFilterMode.FilterRow">
<GridColumns>
<GridColumn Field="@(nameof(SampleData.Id))" Width="150px" />
<GridColumn Field="@(nameof(SampleData.Name))" Title="Employee Name" />
<GridColumn Field="@(nameof(SampleData.Team))" Title="Team" />
<GridColumn Field="@(nameof(SampleData.HireDate))" Title="Hire Date" />
</GridColumns>
</TelerikGrid>
@code {
private TelerikGrid<SampleData> GridRef { get; set; }
private async Task SetGridFilter()
{
GridState<SampleData> desiredState = new GridState<SampleData>()
{
FilterDescriptors = new List<IFilterDescriptor>()
{
new CompositeFilterDescriptor(){
FilterDescriptors = new FilterDescriptorCollection()
{
new FilterDescriptor() { Member = "Id", Operator = FilterOperator.IsGreaterThan, Value = 10, MemberType = typeof(int)}
}
},
new CompositeFilterDescriptor()
{
FilterDescriptors = new FilterDescriptorCollection()
{
new FilterDescriptor() { Member = "Team", Operator = FilterOperator.Contains, Value = "3", MemberType = typeof(string) },
}
}
}
};
await GridRef.SetStateAsync(desiredState);
}
private IEnumerable<SampleData> GridData = Enumerable.Range(1, 30).Select(x => new SampleData
{
Id = x,
Name = "name " + x,
Team = "team " + x % 5,
HireDate = DateTime.Now.AddDays(-x).Date
});
public class SampleData
{
public int Id { get; set; }
public string Name { get; set; }
public string Team { get; set; }
public DateTime HireDate { get; set; }
}
}
@* This snippet shows how to set filtering state to the grid from your code
Applies to the FilterMenu mode *@
@using Telerik.DataSource;
<TelerikButton ThemeColor="primary" OnClick="@SetGridFilter">set filtering from code</TelerikButton>
<TelerikGrid Data="@MyData" Height="400px" @ref="@Grid"
Pageable="true" FilterMode="@GridFilterMode.FilterMenu">
<GridColumns>
<GridColumn Field="@(nameof(SampleData.Id))" Width="120px" />
<GridColumn Field="@(nameof(SampleData.Name))" Title="Employee Name" />
<GridColumn Field="@(nameof(SampleData.Team))" Title="Team" />
<GridColumn Field="@(nameof(SampleData.HireDate))" Title="Hire Date" />
</GridColumns>
</TelerikGrid>
@code {
public TelerikGrid<SampleData> Grid { get; set; }
async Task SetGridFilter()
{
GridState<SampleData> desiredState = new GridState<SampleData>()
{
FilterDescriptors = new List<IFilterDescriptor>()
{
new CompositeFilterDescriptor()
{
FilterDescriptors = new FilterDescriptorCollection()
{
new FilterDescriptor() { Member = "Id", Operator = FilterOperator.IsGreaterThan, Value = 5, MemberType = typeof(int) },
new FilterDescriptor() { Member = "Id", Operator = FilterOperator.IsLessThan, Value = 20, MemberType = typeof(int) },
},
LogicalOperator = FilterCompositionLogicalOperator.And
},
new CompositeFilterDescriptor()
{
FilterDescriptors = new FilterDescriptorCollection()
{
new FilterDescriptor() { Member = "Team", Operator = FilterOperator.Contains, Value = "3", MemberType = typeof(string) },
}
}
}
};
await Grid.SetStateAsync(desiredState);
}
public IEnumerable<SampleData> MyData = Enumerable.Range(1, 30).Select(x => new SampleData
{
Id = x,
Name = "name " + x,
Team = "team " + x % 5,
HireDate = DateTime.Now.AddDays(-x).Date
});
public class SampleData
{
public int Id { get; set; }
public string Name { get; set; }
public string Team { get; set; }
public DateTime HireDate { get; set; }
}
}
@using Telerik.DataSource;
<TelerikButton ThemeColor="primary" OnClick="@SetGridGroup">set grouping from code</TelerikButton>
<TelerikGrid Data="@MyData" Height="400px" @ref="@Grid" Groupable="true"
Pageable="true" FilterMode="@GridFilterMode.FilterMenu">
<GridColumns>
<GridColumn Field="@(nameof(SampleData.Id))" Width="120px" />
<GridColumn Field="@(nameof(SampleData.Name))" Title="Employee Name" />
<GridColumn Field="@(nameof(SampleData.Team))" Title="Team" />
<GridColumn Field="@nameof(SampleData.IsOnLeave)" Title="On Vacation" />
<GridColumn Field="@(nameof(SampleData.HireDate))" Title="Hire Date" />
</GridColumns>
</TelerikGrid>
@code {
public TelerikGrid<SampleData> Grid { get; set; }
async Task SetGridGroup()
{
GridState<SampleData> desiredState = new GridState<SampleData>()
{
GroupDescriptors = new List<GroupDescriptor>()
{
new GroupDescriptor()
{
Member = "Team",
MemberType = typeof(string)
},
new GroupDescriptor()
{
Member = "IsOnLeave",
MemberType = typeof(bool),
SortDirection = ListSortDirection.Descending // not required, but a feature not yet available through the UI
}
},
// choose indexes of groups to be collapsed (they are all expanded by default)
CollapsedGroups = new List<int>() { 0 },
};
await Grid.SetStateAsync(desiredState);
}
public IEnumerable<SampleData> MyData = Enumerable.Range(1, 30).Select(x => new SampleData
{
Id = x,
Name = "name " + x,
Team = "team " + x % 5,
IsOnLeave = x % 2 == 0,
HireDate = DateTime.Now.AddDays(-x).Date
});
public class SampleData
{
public int Id { get; set; }
public string Name { get; set; }
public string Team { get; set; }
public bool IsOnLeave { get; set; }
public DateTime HireDate { get; set; }
}
}
@using Telerik.DataSource;
<TelerikButton ThemeColor="primary" OnClick="@ExpandHierarchy">Expand hierarchy from code</TelerikButton>
<TelerikGrid Data="salesTeamMembers" @ref="Grid">
<DetailTemplate>
@{
var employee = context as MainModel;
<TelerikGrid Data="employee.Orders" Pageable="true" PageSize="5">
<GridColumns>
<GridColumn Field="OrderId"></GridColumn>
<GridColumn Field="DealSize"></GridColumn>
</GridColumns>
</TelerikGrid>
}
</DetailTemplate>
<GridColumns>
<GridColumn Field="Id"></GridColumn>
<GridColumn Field="Name"></GridColumn>
</GridColumns>
</TelerikGrid>
@code {
public TelerikGrid<MainModel> Grid { get; set; }
async Task ExpandHierarchy()
{
GridState<MainModel> desiredState = new GridState<MainModel>()
{
//expand the first two rows
ExpandedItems = new List<MainModel>
{
salesTeamMembers[0],
salesTeamMembers[1]
}
};
await Grid.SetStateAsync(desiredState);
}
List<MainModel> salesTeamMembers { get; set; }
protected override void OnInitialized()
{
salesTeamMembers = GenerateData();
}
private List<MainModel> GenerateData()
{
List<MainModel> data = new List<MainModel>();
for (int i = 0; i < 5; i++)
{
MainModel mdl = new MainModel { Id = i, Name = $"Name {i}" };
mdl.Orders = Enumerable.Range(1, 15).Select(x => new DetailsModel { OrderId = x, DealSize = x ^ i }).ToList();
data.Add(mdl);
}
return data;
}
public class MainModel
{
public int Id { get; set; }
public string Name { get; set; }
public List<DetailsModel> Orders { get; set; }
}
public class DetailsModel
{
public int OrderId { get; set; }
public double DealSize { get; set; }
}
}
Until version
3.1.0
, there was a bug that made the component create defaultFilterDecriptor
s in its state for all columns whenFilterMode
isFilterMenu
, including when no filtering occurs at all. This side-effect allowed filter templates to work seamlessly and with little code. After version3.1.0
, ths bug was fixed and emptyFilterDecriptor
s are not added to the state of the component if no filtering occurs (the way filter templates work is unchanged).
If you want to alter filters for a column, you must either modify the existing descriptor, or replace it. Simply adding an additional
FilterDescriptor
will not show up in the UI, because the Grid uses the first descriptor for the given field for the filtering UI. Another implication is that the Grid state always contains filter descriptors, no matter if the user has filtered or not. Inactive filter descriptors are distinguished by theirnull
Value
.
Handling filter changes - unexpected addition that does not update the UI, replacement of one filter, replacecment of all filters
@using Telerik.DataSource
To see the full effects of this behavior, filter a column such as the ID being less than or equal to 4.
<br /> Then, click a button to see the different behavior each will have:
<ul>
<li>
the first button will not show the new filter in the Team column header
<TelerikButton OnClick="@WrongFilterAdd">Add filter without replace - you will not see the change in the filter UI but the data will change</TelerikButton>
</li>
<li>
the second button will keep the ID column filter and add the Team filter
<TelerikButton OnClick="@SetFilterWithReplaceInCollection">Replace only Team filter</TelerikButton>
</li>
<li>
the third button will remove all filters and set only the custom Team filter
<TelerikButton OnClick="@SetFilterNewCollection">Replace all filters to filter by Team</TelerikButton>
</li>
</ul>
This flexibility lets you choose what behavior you want from the grid.
<TelerikGrid Data="@MyData" Height="400px" @ref="@Grid"
Pageable="true" Sortable="true"
FilterMode="@GridFilterMode.FilterMenu">
<GridColumns>
<GridColumn Field="@(nameof(SampleData.Id))" Width="120px" />
<GridColumn Field="@(nameof(SampleData.Name))" Title="Employee Name" />
<GridColumn Field="@(nameof(SampleData.Team))" Title="Team" />
<GridColumn Field="@(nameof(SampleData.HireDate))" Title="Hire Date" />
</GridColumns>
</TelerikGrid>
@code{
// adds a filter without affecting the UI
async Task WrongFilterAdd()
{
// get state
var state = Grid.GetState();
// simply add a filter to the existing state and existing (default) filters
// PROBLEM - YOU WILL NOT SEE IT HIHGLIGHT THE UI BECAUSE THERE IS A DEFAULT FILTER ALREADY
state.FilterDescriptors.Add(new CompositeFilterDescriptor()
{
FilterDescriptors = new FilterDescriptorCollection()
{
new FilterDescriptor
{
Member = "Team",
Operator = FilterOperator.IsEqualTo,
Value = "team 1",
MemberType = typeof(string)
},
new FilterDescriptor()
{
Member = "Team",
Operator = FilterOperator.IsEqualTo,
Value = null,
MemberType = typeof(string)
}
},
LogicalOperator = FilterCompositionLogicalOperator.Or
});
// set state
await Grid.SetStateAsync(state);
}
// adds a filter
async Task SetFilterWithReplaceInCollection()
{
// get the current state
var state = Grid.GetState();
// remove the current filters for the Team field
// if you don't do this, the default or exiting fitlers will command the UI
// and you may not see the change from the new filter you will add
// see the extension methods related to GetFiltersForMember in the adjacent code tab
// create a new collection so we can iterate over it without it getting lost with the removal from the parent collection
List<IFilterDescriptor> teamFilters = new List<IFilterDescriptor>(state.FilterDescriptors.GetFiltersForMember("Team"));
// remove the existing filters for the Team field from the current collection
for (int i = 0; i < teamFilters.Count(); i++)
{
state.FilterDescriptors.Remove(teamFilters.ElementAt(i) as IFilterDescriptor);
}
// create the desired new filter for the Team field
CompositeFilterDescriptor theFilterDescriptor = new CompositeFilterDescriptor()
{
FilterDescriptors = new FilterDescriptorCollection()
{
new FilterDescriptor
{
Member = "Team",
Operator = FilterOperator.IsEqualTo,
Value = "team 2",
MemberType = typeof(string)
},
new FilterDescriptor()
{
Member = "Team",
Operator = FilterOperator.IsEqualTo,
Value = null,
MemberType = typeof(string)
}
},
LogicalOperator = FilterCompositionLogicalOperator.Or
};
// add the new filter so that it replaces the original filter(s)
state.FilterDescriptors.Add(theFilterDescriptor);
// set the updated state
await Grid.SetStateAsync(state);
}
// replaces all filters
async Task SetFilterNewCollection()
{
// get the current state
var state = Grid.GetState();
//replace the entire filters collection so it only has the new desired filter
state.FilterDescriptors = new List<IFilterDescriptor>()
{
new CompositeFilterDescriptor()
{
FilterDescriptors = new FilterDescriptorCollection()
{
new FilterDescriptor()
{
Member = "Team",
Operator = FilterOperator.Contains,
Value = "3",
MemberType = typeof(string)
},
new FilterDescriptor()
{
Member = "Team",
Operator = FilterOperator.IsEqualTo,
Value = null,
MemberType = typeof(string)
}
},
LogicalOperator = FilterCompositionLogicalOperator.Or
}
};
// set the new state
await Grid.SetStateAsync(state);
}
}
@code {
TelerikGrid<SampleData> Grid { get; set; }
public IEnumerable<SampleData> MyData = Enumerable.Range(1, 30).Select(x => new SampleData
{
Id = x,
Name = "name " + x,
Team = "Team " + x % 5,
HireDate = DateTime.Now.AddDays(-x).Date
});
public class SampleData
{
public int Id { get; set; }
public string Name { get; set; }
public string Team { get; set; }
public DateTime HireDate { get; set; }
}
}
using Telerik.DataSource;
//make sure the namespace matches your page, or add an appropriate using
public static class FilterExtensions
{
public static IEnumerable<CompositeFilterDescriptor> GetCompositeFiltersForMember(this IEnumerable<IFilterDescriptor> filtersCollection, string member)
{
var compositeFilters = filtersCollection.OfType<CompositeFilterDescriptor>();
return compositeFilters.GetFiltersForMember(member).Cast<CompositeFilterDescriptor>();
}
public static IEnumerable<IFilterDescriptor> GetAllFiltersForMember(this IEnumerable<IFilterDescriptor> filtersCollection, string member)
{
return filtersCollection.SelectMemberDescriptors().GetFiltersForMember(member);
}
public static IEnumerable<IFilterDescriptor> GetFiltersForMember(this IEnumerable<IFilterDescriptor> filtersCollection, string member)
{
return filtersCollection.Where(filter => filter.GetFilterMember() == member) ??
Enumerable.Empty<IFilterDescriptor>();
}
public static string GetFilterMember(this IFilterDescriptor filter)
{
var filterDescriptor = filter;
var isFilterDescriptor = filterDescriptor is FilterDescriptor;
while (!isFilterDescriptor)
{
var compositeDescriptor = filterDescriptor as CompositeFilterDescriptor;
if (compositeDescriptor == null)
{
break;
}
filterDescriptor = compositeDescriptor.FilterDescriptors?.ElementAtOrDefault(0);
isFilterDescriptor = filterDescriptor is FilterDescriptor;
}
return ((FilterDescriptor)filterDescriptor)?.Member;
}
}
Set Default (Initial) State
If you want the Grid to start with certain settings for your users, you can pre-define them in the OnStateInit
event.
Note that the filtering configuration depends on the FilterMode
. Row filtering works with FilterDescriptor
s, while menu filtering requires CompositeFilterDescriptor
s.
Choose a default state of the grid for your users
@* Set default (initial) state of the grid
In this example, the records with ID < 5 will be shown, and the Name field will be sorted descending *@
@using Telerik.DataSource
<TelerikGrid Data="@GridData"
Sortable="true"
FilterMode="@GridFilterMode.FilterRow"
AutoGenerateColumns="true"
OnStateInit="@((GridStateEventArgs<SampleData> args) => OnStateInitHandler(args))">
</TelerikGrid>
@code {
private async Task OnStateInitHandler(GridStateEventArgs<SampleData> args)
{
var state = new GridState<SampleData>
{
SortDescriptors = new List<SortDescriptor>
{
new SortDescriptor{ Member = "Name", SortDirection = ListSortDirection.Descending }
},
FilterDescriptors = new List<IFilterDescriptor>()
{
new CompositeFilterDescriptor(){
FilterDescriptors = new FilterDescriptorCollection()
{
new FilterDescriptor() { Member = "Id", Operator = FilterOperator.IsLessThan, Value = 5, MemberType = typeof(int) }
}
}
}
};
args.GridState = state;
}
private IEnumerable<SampleData> GridData = Enumerable.Range(1, 30).Select(x => new SampleData
{
Id = x,
Name = "name " + x,
Team = "team " + x % 5,
HireDate = DateTime.Now.AddDays(-x).Date
});
public class SampleData
{
public int Id { get; set; }
public string Name { get; set; }
public string Team { get; set; }
public DateTime HireDate { get; set; }
}
}
Get and Override User Action That Changes The Grid
Sometimes you may want to know what the user changed in the grid (e.g., when they filter, sort and so on) and even override those operations. One way to do that is to monitor the OnRead
event, cache the previous DataSourceRequest
argument, compare against it, alter it if needed, and implement the operations yourself. Another is to use the OnStateChanged
event.
The example below shows the latter. Review the code comments to see how it works and to make sure you don't get issues. You can find another example of overriding the user actions in the Static Grid Group Knowledge Base article.
Know when the grid state changes, which parameter changes, and amend the change
@* This example does the following:
* Logs to the console what changed in the grid
* If the user changes the Name column filtering, the filter is always overriden to "Contains" and its value to "name 1"
* if there is no filter on the ID column, the ID column is filtered with ID < 15.
To test it out, try filtering the name column
*@
@using Telerik.DataSource
<TelerikGrid Data="@GridData"
@ref="GridRef"
Sortable="true"
FilterMode="@GridFilterMode.FilterRow"
AutoGenerateColumns="true"
Pageable="true"
OnStateChanged="@((GridStateEventArgs<SampleData> args) => OnStateChangedHandler(args))">
</TelerikGrid>
@code {
private TelerikGrid<SampleData> GridRef { get; set; }
// Note: This can cause a performance delay if you do long operations here
// Note 2: The grid does not await this event, its purpose is to notify you of changes
// so you must not perform async operations and data loading here, or issues with the grid state may occur
// or other things you change on the page won't actually change. The .SetStateAsync() call redraws only the grid, but not the rest of the page
private async Task OnStateChangedHandler(GridStateEventArgs<SampleData> args)
{
Console.WriteLine(args.PropertyName); // get the setting that was just changed (paging, sorting, filtering...)
if (args.PropertyName == "FilterDescriptors") // filtering changed for our example
{
// ensure certain state based on some condition
// in this example - ensure that the ID field is always filtered with a certain setting unless the user filters it explicitly
bool isIdFiltered = false;
foreach (CompositeFilterDescriptor compositeFilter in args.GridState.FilterDescriptors)
{
foreach (FilterDescriptor filter in compositeFilter.FilterDescriptors)
{
if (filter.Member == "Id")
{
isIdFiltered = true;
}
// you could override a user action as well - change settings on the corresponding parameter
// make sure that the .SetStateAsync() method of the grid is always called if you do that
if (filter.Member == "Name")
{
filter.Value = "name 1";
filter.Operator = FilterOperator.Contains;
}
}
}
if (!isIdFiltered)
{
args.GridState.FilterDescriptors.Add(
new CompositeFilterDescriptor()
{
FilterDescriptors = new FilterDescriptorCollection()
{
new FilterDescriptor() {
Member = "Id",
MemberType = typeof(int),
Operator = FilterOperator.IsLessThan,
Value = 15
}
}
});
}
// needed only if you will be overriding user actions or amending them
// if you only need to be notified of changes, you should not call this method
await GridRef.SetStateAsync(args.GridState);
}
}
private IEnumerable<SampleData> GridData = Enumerable.Range(1, 300).Select(x => new SampleData
{
Id = x,
Name = "name " + x,
Team = "team " + x % 5,
HireDate = DateTime.Now.AddDays(-x).Date
});
public class SampleData
{
public int Id { get; set; }
public string Name { get; set; }
public string Team { get; set; }
public DateTime HireDate { get; set; }
}
}
Initiate Editing or Inserting of an Item
The Grid state lets you store the item that the user is currently working on - both an existing model that is being edited, and a new item the user is inserting. This happens automatically when you save the Grid state. If you want to save on every keystroke instead of on OnChange
- use a custom editor template and update the EditItem
or InsertedItem
of the state object as required, then save the state into your service.
In addition to that, you can also use the EditItem
, OriginalEditItem
and InsertItem
fields of the state object to put the Grid in edit/insert mode through your own application code, instead of needing the user to initiate this through a command button.
Put and item in Edit mode or start Inserting a new item
@* This example shows how to trigger Grid Edit, Create, Save and Cancel operations programmatically.
The buttons that initiate these operations can be anywhere on the page, including inside the Grid.
Note the model constructors and static method that show how to get a new instance for the edit item.
*@
<TelerikButton OnClick="@StartInsert">Start Insert operation</TelerikButton>
<TelerikButton OnClick="@EditItemFour">Put item 4 in Edit mode</TelerikButton>
<TelerikButton OnClick="@SaveAndClose">Save And Close</TelerikButton>
<TelerikButton OnClick="@CancelEditing">Cancel Editing</TelerikButton>
<TelerikGrid Data=@MyData EditMode="@GridEditMode.Inline" Pageable="true" Height="500px" @ref="@GridRef"
OnUpdate="@UpdateHandler" OnCreate="@CreateHandler">
<GridColumns>
<GridColumn Field=@nameof(SampleData.ID) Title="ID" Editable="false" />
<GridColumn Field=@nameof(SampleData.Name) Title="Name" />
<GridCommandColumn>
<GridCommandButton Command="Save" Icon="@FontIcon.Save" ShowInEdit="true">Update</GridCommandButton>
<GridCommandButton Command="Edit" Icon="@FontIcon.Pencil">Edit</GridCommandButton>
<GridCommandButton Command="Delete" Icon="@FontIcon.Trash">Delete</GridCommandButton>
<GridCommandButton Command="Cancel" Icon="@FontIcon.Cancel" ShowInEdit="true">Cancel</GridCommandButton>
</GridCommandColumn>
</GridColumns>
</TelerikGrid>
@code {
TelerikGrid<SampleData> GridRef { get; set; }
async Task StartInsert()
{
var currState = GridRef.GetState();
// reset any current editing. Not mandatory.
currState.EditItem = null;
currState.OriginalEditItem = null;
// add new inserted item to the state, then set it to the grid
// you can predefine values here as well (not mandatory)
currState.InsertedItem = new SampleData() { Name = "some predefined value" };
await GridRef.SetStateAsync(currState);
// note: possible only for Inline and Popup edit modes, with InCell there is never an inserted item, only edited items
}
async Task EditItemFour()
{
var currState = GridRef.GetState();
// reset any current insertion and any old edited items. Not mandatory.
currState.InsertedItem = null;
// add item you want to edit to the state, then set it to the grid
SampleData originalItem = MyData.Where(itm => itm.ID == 4).FirstOrDefault();
SampleData itemToEdit = SampleData.GetClonedInstance(originalItem);
// you can alter values here as well (not mandatory)
//itemToEdit.Name = "Changed from code";
currState.EditItem = itemToEdit;
currState.OriginalEditItem = originalItem;
// for InCell editing, you can use the EditField property instead
await GridRef.SetStateAsync(currState);
}
async Task SaveAndClose()
{
var gridState = GridRef.GetState();
// distinguish Update and Create operations via the Grid state
var itemToSave = gridState.EditItem ?? gridState.InsertedItem;
// call the correct data service method for Update or Create
if (gridState.InsertedItem != null)
{
await CreateHandler(new GridCommandEventArgs()
{
IsNew = true,
Item = itemToSave
});
}
else
{
await UpdateHandler(new GridCommandEventArgs()
{
Item = itemToSave
});
}
// reset all edit-related state properties
gridState.EditItem = null;
gridState.OriginalEditItem = null;
gridState.InsertedItem = null;
await GridRef.SetState(gridState);
}
async Task CancelEditing()
{
var gridState = GridRef.GetState();
// reset all edit-related state properties
gridState.EditItem = null;
gridState.OriginalEditItem = null;
gridState.InsertedItem = null;
await GridRef.SetState(gridState);
}
// Sample CRUD operations and data follow
async Task UpdateHandler(GridCommandEventArgs args)
{
SampleData item = (SampleData)args.Item;
// perform actual data source operations here through your service
await MyService.Update(item);
// update the local view-model data with the service data
await GetGridData();
}
async Task CreateHandler(GridCommandEventArgs args)
{
SampleData item = (SampleData)args.Item;
// perform actual data source operation here through your service
await MyService.Create(item);
// update the local view-model data with the service data
await GetGridData();
}
// Sample class definition - note the constructors, overrides and comments
public 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;
}
// define constructors and a static method so we can deep clone instances
// we use that to define the edited item - otherwise the references will point
// to the item in the grid data sources and all changes will happen immediately on
// the Data collection, and we don't want that - so we need a deep clone with its own reference
// this is just one way to implement this, you can do it in a different way
public SampleData()
{
}
public SampleData(SampleData itmToClone)
{
this.ID = itmToClone.ID;
this.Name = itmToClone.Name;
}
public static SampleData GetClonedInstance(SampleData itmToClone)
{
return new SampleData(itmToClone);
}
}
public List<SampleData> MyData { get; set; }
async Task GetGridData()
{
MyData = await MyService.Read();
}
protected override async Task OnInitializedAsync()
{
await GetGridData();
}
// the following static class mimics an actual data service that handles the actual data source
// replace it with your actual service through the DI, this only mimics how the API can look like and works for this standalone page
public static class MyService
{
private static List<SampleData> _data { get; set; } = new List<SampleData>();
public static async Task Create(SampleData itemToInsert)
{
itemToInsert.ID = _data.Count + 1;
_data.Insert(0, itemToInsert);
}
public static async Task<List<SampleData>> Read()
{
if (_data.Count < 1)
{
for (int i = 1; i < 50; i++)
{
_data.Add(new SampleData()
{
ID = i,
Name = "Name " + i.ToString()
});
}
}
return await Task.FromResult(_data);
}
public static async Task Update(SampleData itemToUpdate)
{
var index = _data.FindIndex(i => i.ID == itemToUpdate.ID);
if (index != -1)
{
_data[index] = itemToUpdate;
}
}
public static async Task Delete(SampleData itemToDelete)
{
_data.Remove(itemToDelete);
}
}
}
Get Current Columns Visibility, Order, Field
The ColumnStates
property of the GridState object provides you with information about the current state of the Grid columns. It contains the following properties:
Field | Type | Description |
---|---|---|
Index |
int |
the current index of the column based on the position the user chose |
Id |
string |
the Id of the column if it is set |
Field |
string |
the field of the column |
Visible |
bool? |
whether the column is hidden or not |
Locked |
bool |
whether the column is locked or not |
Width |
string |
the width of the column if it is set |
By looping over the ColumnStates
collection you can know what the user sees. By default, the order of the columns in the state collection will remain the same but their Index
value will be changed to indicate their position. You can, for example, sort by the index and filter by the visibility of the columns to get the approximate view the user sees.
Obtain the current columns visibility, rendering order, locked state and field name
@* Click the button, reorder some columns, maybe lock one of them, hide another, and click the button
again to see how the state changes but the order of the columns in the state collection remains the same.*@
<TelerikButton OnClick="@GetCurrentColumns">Get Columns order and parameters</TelerikButton>
<TelerikGrid Data="@GridData"
AutoGenerateColumns="true"
@ref="@Grid"
ShowColumnMenu="true"
Reorderable="true"
Pageable="true">
</TelerikGrid>
@( new MarkupString(ColumnsLog) )
@code {
IEnumerable<Person> GridData { get; set; }
TelerikGrid<Person> Grid { get; set; }
string ColumnsLog { get; set; } = string.Empty;
protected override void OnInitialized()
{
GridData = GetGridData();
base.OnInitialized();
}
public void GetCurrentColumns()
{
ColumnsLog = string.Empty;
var columnsState = Grid.GetState().ColumnStates;
foreach (var columnState in columnsState)
{
// human readable info for visibility information
var visible = columnState.Visible != false;
string log = $"<p>Column: <strong>{columnState.Field}</strong> | Index in state:{columnState.Index} | Visible: {visible} | Locked: {columnState.Locked}</p>";
ColumnsLog += log;
}
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
public IEnumerable<Person> GetGridData()
{
var data = new List<Person>();
for (int i = 0; i < 30; i++)
{
data.Add(new Person { Id = i, Name = $"Name {i}", Age = i + 20 });
}
return data;
}
}