From b59327efae71d5cc21765c520fc8465af0dc4801 Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Wed, 14 Oct 2020 11:21:12 +0300 Subject: [PATCH 01/19] feat(treelist): state inititla --- components/treelist/state.md | 583 +++++++++++++++++++++++++++++++++++ 1 file changed, 583 insertions(+) diff --git a/components/treelist/state.md b/components/treelist/state.md index ba9a7be716..c1cf6c7638 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -10,3 +10,586 @@ position: 50 # TreeList State +The TreeList 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: TreeList State](https://demos.telerik.com/blazor-ui/treelist/persist-state). + +This article contains the following sections: + + + +* [Basics](#basics) + * [Events](#events) + * [Methods](#methods) +* [Information in the Grid State](#information-in-the-grid-state) +* [Examples](#examples) + * [Save and Load Grid State from Browser LocalStorage](#save-and-load-grid-state-from-browser-localstorage) + * [Set Grid Options Through State](#set-grid-options-through-state) + * [Set Default (Initial) State](#set-default-initial-state) + * [Get and Override User Action That Changes The Grid](#get-and-override-user-action-that-changes-the-grid) + * [Initiate Editing or Inserting of an Item](#initiate-editing-or-inserting-of-an-item) + + + + + +## 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 the `GridState` field of its event arguments. + +* `OnStateChanged` fires when the user makes a change to the grid state (such as dragging to group by a field, filtering a column, editing, selecting and so on). The `GridState` field of the event arguments provides the current grid state so you can store it. The `PropertyName` field of the event arguments indicates what is the aspect that changed. + * We recommend that you use an `async void` handler for the `OnStateChanged` 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. + +By combining these two events you can save the grid layout for your users automatically by only calling upon your storage service in the respective method. + +### Methods + +The `GetState` and `SetState` 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. + +* `SetState` 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 `SaveState`. + +If you want to put the grid in a certain configuration without preserving the old one, create a `new GridState()` and apply the settings there, then pass it to `SetState`. + +To reset the grid state, call `SetState(null)`. + +## Information in the Grid State + +The following information is present in the grid state: + +* Columns - visibility, width, index (order) of the column. The grid matches the columns from its declaration with the columns list in the state object, in the same order, so the grid must initialize with the same collection of columns that were used to save the 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). The `OriginalEditItem` 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). +* Grouping - group descriptors (fields by which the grid is grouped), collapsed group indexes. +* Paging - page index, offset (skip) for virtual scrolling. +* Rows - indexes of expanded detail templates. +* Sorting - sort descriptors (fields by which the grid is sorted, and the direction). +* Selection - list of selected items. + + +## Examples + +### 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. + +>caption Save, Load, Reset grid state on every state change. Uses a sample LocalStorage in the browser. + +````Component +@inject LocalStorage LocalStorage +@inject IJSRuntime JsInterop +@using Telerik.DataSource; + +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. +
+ +Reload the page to see the current grid state preserved +Reset the state + + + + + + + + Edit + Delete + Save + Cancel + + + + Add Employee + + + +@if (SelectedItems != null) +{ + +} + +@code { + // Load and Save the state through the grid events + + string UniqueStorageKey = "SampleGridStateStorageThatShouldBeUnique"; + + async Task OnStateInitHandler(GridStateEventArgs args) + { + try + { + var state = await LocalStorage.GetItem>(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 args) + { + await LocalStorage.SetItem(UniqueStorageKey, args.GridState); + } + + TelerikGrid Grid { get; set; } + async Task ResetState() + { + // clean up the storage + await LocalStorage.RemoveItem(UniqueStorageKey); + + await Grid.SetState(null); // pass null to reset the state + } + + void ReloadPage() + { + JsInterop.InvokeVoidAsync("window.location.reload"); + } + + // Sample CRUD operations + + private void CreateItem(GridCommandEventArgs args) + { + var argsItem = args.Item as SampleData; + + argsItem.Id = GridData.Count + 1; + + GridData.Insert(0, argsItem); + } + + private void DeleteItem(GridCommandEventArgs args) + { + var argsItem = args.Item as SampleData; + + GridData.Remove(argsItem); + } + + private void UpdateItem(GridCommandEventArgs args) + { + var argsItem = args.Item as SampleData; + var index = GridData.FindIndex(i => i.Id == argsItem.Id); + if (index != -1) + { + GridData[index] = argsItem; + } + } + + // Sample data follows below + + public IEnumerable SelectedItems { get; set; } = Enumerable.Empty(); + + public List GridData { get; set; } = Enumerable.Range(1, 30).Select(x => new SampleData + { + Id = x, + Name = "name " + x, + Team = "team " + x % 5 + }).ToList(); + + public class SampleData + { + public int Id { get; set; } + public string Name { get; set; } + public string Team { 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; + } + } +} +```` +````Service +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 GetItem(string key) + { + var data = await JSRuntimeInstance.InvokeAsync("localStorage.getItem", key); + if (!string.IsNullOrEmpty(data)) + { + return JsonSerializer.Deserialize(data); + } + + return default; + } + + public ValueTask RemoveItem(string key) + { + return JSRuntimeInstance.InvokeVoidAsync("localStorage.removeItem", key); + } +} +```` + +### 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. + +>tip The individual tabs below show how you can use the state to programmatically set the grid filtering, sorting, grouping and other features. + +@[template](/_contentTemplates/grid/state.md#initial-state) + + +````Sorting +@[template](/_contentTemplates/grid/state.md#set-sort-from-code) +```` +````FilterRow +@[template](/_contentTemplates/grid/state.md#filter-row-from-code) +```` +````FilterMenu +@[template](/_contentTemplates/grid/state.md#filter-menu-from-code) +```` +````Grouping +@[template](/_contentTemplates/grid/state.md#group-from-code) +```` +````Hierarchy +@[template](/_contentTemplates/grid/state.md#expand-hierarchy-from-code) +```` + +### Set Default (Initial) State + +If you want the grid to start with certain settings for your end users, you can pre-define them in the `OnStateInit event`. + +>caption Choose a default state of the grid for your users + +````CSHTML +@* 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 *@ + + + + +@code { + async Task OnStateInitHandler(GridStateEventArgs args) + { + var state = new GridState + { + SortDescriptors = new List + { + new Telerik.DataSource.SortDescriptor{ Member = "Name", SortDirection = Telerik.DataSource.ListSortDirection.Descending } + }, + FilterDescriptors = new List() + { + new Telerik.DataSource.FilterDescriptor() { Member = "Id", Operator = Telerik.DataSource.FilterOperator.IsLessThan, Value = 5, MemberType = typeof(int) }, + } + }; + + args.GridState = state; + } + + public IEnumerable 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; } + } +} +```` + +### 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`]({%slug components/grid/manual-operations%}#cache-data-request) event, cache the previous `DataSourceRequest`, 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]({%slug grid-kb-static-group%}) Knowledge Base article. + +>caption Know when the grid state changes, which parameter changes, and amend the change + +````CSHTML +@* 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 + + + + +@code { + TelerikGrid 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 .SetState() call redraws only the grid, but not the rest of the page + async void OnStateChangedHandler(GridStateEventArgs args) + { + Console.WriteLine(args.PropertyName); // get the setting that was just changed (paging, sorting,...) + + if (args.PropertyName == "FilterDescriptors") // sorting 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 (FilterDescriptor item in args.GridState.FilterDescriptors) + { + if(item.Member == "Id") + { + isIdFiltered = true; + } + + // you could override a user action as well - change settings on the corresponding parameter + // make sure that the .SetState() method of the grid is always called if you do that + if(item.Member == "Name") + { + item.Value = "name 1"; + item.Operator = FilterOperator.Contains; + } + } + if (!isIdFiltered) + { + args.GridState.FilterDescriptors.Add(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.SetState(args.GridState); + } + } + + public IEnumerable MyData = 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]({%slug components/grid/columns/command%}). + +>caption Put and item in Edit mode or start Inserting a new item + +````CSHTML +@* This example shows how to make the grid edit a certain item or start insert operation + through your own code, without requiring the user to click the Command buttons. + The buttons that initiate these operations can be anywhere on the page, inlcuding inside the grid. + Note the model constructors and static method that show how to get a new instance for the edit item + *@ + +Start Insert operation +Put item 4 in Edit mode + + + + + + + Update + Edit + Delete + Cancel + + + + +@code { + TelerikGrid 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.SetState(currState); + } + + 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 itemToEdit = SampleData.GetClonedInstance(MyData.Where(itm => itm.ID == 4).FirstOrDefault()); + // you can alter values here as well (not mandatory) + //itemToEdit.Name = "Changed from code"; + currState.OriginalEditItem = itemToEdit; + // for InCell editing, you can use the EditField property instead + await GridRef.SetState(currState); + } + + + // Sample CRUD operations and data follow + + async Task UpdateHandler(GridCommandEventArgs args) + { + SampleData item = (SampleData)args.Item; + var index = MyData.FindIndex(i => i.ID == item.ID); + if (index != -1) + { + MyData[index] = item; + } + } + + async Task DeleteHandler(GridCommandEventArgs args) + { + SampleData item = (SampleData)args.Item; + MyData.Remove(item); + } + + async Task CreateHandler(GridCommandEventArgs args) + { + SampleData item = (SampleData)args.Item; + item.ID = MyData.Count + 1; + MyData.Insert(0, item); + + } + + 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 MyData { get; set; } + + protected override void OnInitialized() + { + MyData = new List(); + + for (int i = 0; i < 50; i++) + { + MyData.Add(new SampleData() + { + ID = i, + Name = "Name " + i.ToString() + }); + } + } +} +```` + +## See Also + + * [Live Demo: Grid State](https://demos.telerik.com/blazor-ui/grid/persist-state) + From 129f12908995207f054e3f4e376f61936a492b99 Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Thu, 15 Oct 2020 15:14:59 +0300 Subject: [PATCH 02/19] chore(treelist): state docs --- _contentTemplates/treelist/state.md | 507 ++++++++++++++++++++++++++++ components/treelist/state.md | 438 +++++++++--------------- 2 files changed, 671 insertions(+), 274 deletions(-) create mode 100644 _contentTemplates/treelist/state.md diff --git a/_contentTemplates/treelist/state.md b/_contentTemplates/treelist/state.md new file mode 100644 index 0000000000..e7d2c1e490 --- /dev/null +++ b/_contentTemplates/treelist/state.md @@ -0,0 +1,507 @@ +#initial-state +>tip If you want to set an initial state to the TreeList, use a similar snippet, but in the [`OnStateInit event`]({%slug treelist-state%}#set-default-initial-state) +#end + + +#set-sort-from-code +@* This snippet shows how to set sorting state to the TreeList from your code *@ + +@using Telerik.DataSource; + +Set sorted state + + + + + + + + + + +@code { + public TelerikTreeList TreeListRef { get; set; } = new TelerikTreeList(); + + async Task SetTreeListSort() + { + var sortedState = new TreeListState() + { + SortDescriptors = new List() + { + new SortDescriptor(){ Member = nameof(Employee.Id), SortDirection = ListSortDirection.Descending } + } + }; + + await TreeListRef.SetState(sortedState); + } + + public List Data { get; set; } + + // sample model + + public class Employee + { + // hierarchical data collections + public List DirectReports { get; set; } + + // data fields for display + public int Id { get; set; } + public string Name { get; set; } + public string EmailAddress { get; set; } + public DateTime HireDate { get; set; } + } + + // data generation + + // used in this example for data generation and retrieval for CUD operations on the current view-model data + public int LastId { get; set; } = 1; + + protected override async Task OnInitializedAsync() + { + Data = await GetTreeListData(); + } + + async Task> GetTreeListData() + { + List data = new List(); + + for (int i = 1; i < 15; i++) + { + Employee root = new Employee + { + Id = LastId, + Name = $"root: {i}", + EmailAddress = $"{i}@example.com", + HireDate = DateTime.Now.AddYears(-i), + DirectReports = new List(), // prepare a collection for the child items, will be populated later in the code + }; + data.Add(root); + LastId++; + + for (int j = 1; j < 4; j++) + { + int currId = LastId; + Employee firstLevelChild = new Employee + { + Id = currId, + Name = $"first level child {j} of {i}", + EmailAddress = $"{currId}@example.com", + HireDate = DateTime.Now.AddDays(-currId), + DirectReports = new List(), // collection for child nodes + }; + root.DirectReports.Add(firstLevelChild); // populate the parent's collection + LastId++; + + for (int k = 1; k < 3; k++) + { + int nestedId = LastId; + // populate the parent's collection + firstLevelChild.DirectReports.Add(new Employee + { + Id = LastId, + Name = $"second level child {k} of {j} and {i}", + EmailAddress = $"{nestedId}@example.com", + HireDate = DateTime.Now.AddMinutes(-nestedId) + }); ; + LastId++; + } + } + } + + return await Task.FromResult(data); + } +} +#end + + + +#filter-row-from-code +@* This snippet shows how to set filtering state to the TreeList from your code + Applies to the FilterRow mode *@ + +@using Telerik.DataSource; + +Set filtered state + + + + + + + + + + +@code { + public TelerikTreeList TreeListRef { get; set; } = new TelerikTreeList(); + + async Task SetTreeListFilter() + { + var filteredState = new TreeListState() + { + FilterDescriptors = new List() + { + new FilterDescriptor() + { + Member = nameof(Employee.Id), + MemberType = typeof(int), + Operator = FilterOperator.IsGreaterThan, + Value = 5 + }, + new FilterDescriptor() + { + Member = nameof(Employee.Name), + MemberType = typeof(string), + Operator = FilterOperator.Contains, + Value = "second level" + } + } + }; + + await TreeListRef.SetState(filteredState); + } + + public List Data { get; set; } + + // sample model + + public class Employee + { + // hierarchical data collections + public List DirectReports { get; set; } + + // data fields for display + public int Id { get; set; } + public string Name { get; set; } + public string EmailAddress { get; set; } + public DateTime HireDate { get; set; } + } + + // data generation + + // used in this example for data generation and retrieval for CUD operations on the current view-model data + public int LastId { get; set; } = 1; + + protected override async Task OnInitializedAsync() + { + Data = await GetTreeListData(); + } + + async Task> GetTreeListData() + { + List data = new List(); + + for (int i = 1; i < 15; i++) + { + Employee root = new Employee + { + Id = LastId, + Name = $"root: {i}", + EmailAddress = $"{i}@example.com", + HireDate = DateTime.Now.AddYears(-i), + DirectReports = new List(), // prepare a collection for the child items, will be populated later in the code + }; + data.Add(root); + LastId++; + + for (int j = 1; j < 4; j++) + { + int currId = LastId; + Employee firstLevelChild = new Employee + { + Id = currId, + Name = $"first level child {j} of {i}", + EmailAddress = $"{currId}@example.com", + HireDate = DateTime.Now.AddDays(-currId), + DirectReports = new List(), // collection for child nodes + }; + root.DirectReports.Add(firstLevelChild); // populate the parent's collection + LastId++; + + for (int k = 1; k < 3; k++) + { + int nestedId = LastId; + // populate the parent's collection + firstLevelChild.DirectReports.Add(new Employee + { + Id = LastId, + Name = $"second level child {k} of {j} and {i}", + EmailAddress = $"{nestedId}@example.com", + HireDate = DateTime.Now.AddMinutes(-nestedId) + }); ; + LastId++; + } + } + } + + return await Task.FromResult(data); + } +} +#end + +#filter-menu-from-code +@* This snippet shows how to set filtering state to the TreeList from your code + Applies to the FilterMenu mode *@ + +@using Telerik.DataSource; + +Set filtered state + + + + + + + + + + +@code { + public TelerikTreeList TreeListRef { get; set; } = new TelerikTreeList(); + + async Task SetTreeListFilter() + { + var filteredState = new TreeListState() + { + FilterDescriptors = new List() + { + new FilterDescriptor() + { + Member = nameof(Employee.Id), + MemberType = typeof(int), + Operator = FilterOperator.IsGreaterThan, + Value = 5 + }, + new FilterDescriptor() + { + Member = nameof(Employee.Name), + MemberType = typeof(string), + Operator = FilterOperator.Contains, + Value = "second level" + } + } + }; + + await TreeListRef.SetState(filteredState); + } + + public List Data { get; set; } + + // sample model + + public class Employee + { + // hierarchical data collections + public List DirectReports { get; set; } + + // data fields for display + public int Id { get; set; } + public string Name { get; set; } + public string EmailAddress { get; set; } + public DateTime HireDate { get; set; } + } + + // data generation + + // used in this example for data generation and retrieval for CUD operations on the current view-model data + public int LastId { get; set; } = 1; + + protected override async Task OnInitializedAsync() + { + Data = await GetTreeListData(); + } + + async Task> GetTreeListData() + { + List data = new List(); + + for (int i = 1; i < 15; i++) + { + Employee root = new Employee + { + Id = LastId, + Name = $"root: {i}", + EmailAddress = $"{i}@example.com", + HireDate = DateTime.Now.AddYears(-i), + DirectReports = new List(), // prepare a collection for the child items, will be populated later in the code + }; + data.Add(root); + LastId++; + + for (int j = 1; j < 4; j++) + { + int currId = LastId; + Employee firstLevelChild = new Employee + { + Id = currId, + Name = $"first level child {j} of {i}", + EmailAddress = $"{currId}@example.com", + HireDate = DateTime.Now.AddDays(-currId), + DirectReports = new List(), // collection for child nodes + }; + root.DirectReports.Add(firstLevelChild); // populate the parent's collection + LastId++; + + for (int k = 1; k < 3; k++) + { + int nestedId = LastId; + // populate the parent's collection + firstLevelChild.DirectReports.Add(new Employee + { + Id = LastId, + Name = $"second level child {k} of {j} and {i}", + EmailAddress = $"{nestedId}@example.com", + HireDate = DateTime.Now.AddMinutes(-nestedId) + }); ; + LastId++; + } + } + } + + return await Task.FromResult(data); + } +} +#end + + +#expand-items-from-code +@*Collapse all items but the first one*@ + +@using Telerik.DataSource; + +Expand the first item only + + + + + + + + + + +@code { + public TelerikTreeList TreeListRef { get; set; } = new TelerikTreeList(); + + async Task SetTreeListExpandedItems() + { + var expandedState = new TreeListState() + { + ExpandedItems = new List() + { + Data[0] + } + }; + + await TreeListRef.SetState(expandedState); + } + + public List Data { get; set; } + + // sample model + + public class Employee + { + // hierarchical data collections + public List DirectReports { get; set; } + + // data fields for display + public int Id { get; set; } + public string Name { get; set; } + public string EmailAddress { get; set; } + public DateTime HireDate { get; set; } + } + + // data generation + + // used in this example for data generation and retrieval for CUD operations on the current view-model data + public int LastId { get; set; } = 1; + + protected override async Task OnInitializedAsync() + { + Data = await GetTreeListData(); + } + + async Task> GetTreeListData() + { + List data = new List(); + + for (int i = 1; i < 15; i++) + { + Employee root = new Employee + { + Id = LastId, + Name = $"root: {i}", + EmailAddress = $"{i}@example.com", + HireDate = DateTime.Now.AddYears(-i), + DirectReports = new List(), // prepare a collection for the child items, will be populated later in the code + }; + data.Add(root); + LastId++; + + for (int j = 1; j < 4; j++) + { + int currId = LastId; + Employee firstLevelChild = new Employee + { + Id = currId, + Name = $"first level child {j} of {i}", + EmailAddress = $"{currId}@example.com", + HireDate = DateTime.Now.AddDays(-currId), + DirectReports = new List(), // collection for child nodes + }; + root.DirectReports.Add(firstLevelChild); // populate the parent's collection + LastId++; + + for (int k = 1; k < 3; k++) + { + int nestedId = LastId; + // populate the parent's collection + firstLevelChild.DirectReports.Add(new Employee + { + Id = LastId, + Name = $"second level child {k} of {j} and {i}", + EmailAddress = $"{nestedId}@example.com", + HireDate = DateTime.Now.AddMinutes(-nestedId) + }); ; + LastId++; + } + } + } + + return await Task.FromResult(data); + } +} +#end + diff --git a/components/treelist/state.md b/components/treelist/state.md index c1cf6c7638..08d0add1d5 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -10,83 +10,99 @@ position: 50 # TreeList State -The TreeList 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. +The TreeList lets you save, load and change its current state through code. The state management includes all the user-configurable elements of the TreeList - such as managing the expanded state of the items, sorting, filtering, paging, edited items and selection. You can see this feature in the [Live Demo: TreeList State](https://demos.telerik.com/blazor-ui/treelist/persist-state). This article contains the following sections: - - * [Basics](#basics) * [Events](#events) * [Methods](#methods) -* [Information in the Grid State](#information-in-the-grid-state) +* [Information in the TreeList State](#information-in-the-treelist-state) * [Examples](#examples) - * [Save and Load Grid State from Browser LocalStorage](#save-and-load-grid-state-from-browser-localstorage) - * [Set Grid Options Through State](#set-grid-options-through-state) + * [Save and Load TreeList State from Browser LocalStorage](#save-and-load-treelist-state-from-browser-localstorage) + * [Set TreeList Options Through State](#set-treelist-options-through-state) * [Set Default (Initial) State](#set-default-initial-state) - * [Get and Override User Action That Changes The Grid](#get-and-override-user-action-that-changes-the-grid) + * [Get and Override User Action That Changes The TreeList](#get-and-override-user-action-that-changes-the-treelist) * [Initiate Editing or Inserting of an Item](#initiate-editing-or-inserting-of-an-item) - +## Basics +The TreeList state is a generic class whose type is determined by the type of the data model you use for the TreeList. It contains fields that correspond to the TreeList behaviors which you can use to save, load and modify the component state. -## Basics +Fields that pertain to model data (such as edited item, inserted item, selected items) are also typed according to the TreeList 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 `Data` parameter. 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 component data source. -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. +The TreeList exposes two events and two methods to allow flexible operations over its 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. +* [Events](#events) -The grid offers two events and two methods to allow flexible operations over its state: +* [Methods](#methods) ### 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. +The `OnStateInit` and `OnStateChanged` events are raised by the TreeList 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 the `GridState` field of its event arguments. +* `OnStateInit` fires when the TreeList is initializing and you can provide the state you load from your storage to the `TreeListState` field of its event arguments. -* `OnStateChanged` fires when the user makes a change to the grid state (such as dragging to group by a field, filtering a column, editing, selecting and so on). The `GridState` field of the event arguments provides the current grid state so you can store it. The `PropertyName` field of the event arguments indicates what is the aspect that changed. +* `OnStateChanged` fires when the user makes a change to the TreeList state (such as expanding or collapsing an item, filtering a column, editing, selecting and so on). The `TreeListState` field of the event arguments provides the current state so you can store it. The `PropertyName` field of the event arguments indicates what is the aspect that changed. * We recommend that you use an `async void` handler for the `OnStateChanged` 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. -By combining these two events you can save the grid layout for your users automatically by only calling upon your storage service in the respective method. +By combining these two events you can save the TreeList layout for your users automatically by only calling upon your storage service in the respective method. ### Methods -The `GetState` and `SetState` 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. +The `GetState` and `SetState` instance methods provide flexibility for your business logic. They let you get and set the current TreeList state on demand outside of the component events. + +* `GetState` returns the TreeList state so you can store it only on a certain condition - for example, you may want to save the TreeList layout only on a button click, and not on every user interaction with the component. You can also use it to get information about the current state of the filters, sorts and so on. + +* `SetState` takes an instance of a TreeList state so you can use your own code to alter the component layout and state. For example, you can have a button that puts the TreeList in a certain configuration that helps your users review data (like certain filters, sorts, expanded items, initiate item editing or inserting, etc.). + +If you want to make changes on the current TreeList state, first get it from the component through the `GetState` method, then apply the modifications on the object you got, and pass it to `SaveState`. + +If you want to put the TreeList in a certain configuration without preserving the old one, create a `new TreeListState()` and apply the settings there, then pass it to `SetState`. + +To reset the TreeList state, call `SetState(null)`. + +## Information in the TreeList State -* `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. +The following information is present in the TreeList state: -* `SetState` 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.). +* **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 TreeList - the data is updated in the `OnChange` event, not on every keystroke for performance reasons). The `OriginalEditItem` carries the original model without the user modifications so you can compare. -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 `SaveState`. +* **Filtering** - filter descriptors (fields by which the grid is filtered, the operator and value). -If you want to put the grid in a certain configuration without preserving the old one, create a `new GridState()` and apply the settings there, then pass it to `SetState`. +* **Paging** - page index -To reset the grid state, call `SetState(null)`. +* **Sorting** - sort descriptors (fields by which the TreeList is sorted, and the direction). -## Information in the Grid State +* **Selection** - list of selected items. -The following information is present in the grid state: +* **Columns** - Visible, Width, Index (order) of the column that the user sees, Locked (pinned). -* Columns - visibility, width, index (order) of the column. The grid matches the columns from its declaration with the columns list in the state object, in the same order, so the grid must initialize with the same collection of columns that were used to save the 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). The `OriginalEditItem` 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). -* Grouping - group descriptors (fields by which the grid is grouped), collapsed group indexes. -* Paging - page index, offset (skip) for virtual scrolling. -* Rows - indexes of expanded detail templates. -* Sorting - sort descriptors (fields by which the grid is sorted, and the direction). -* Selection - list of selected items. + * The TreeList 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 TreeList 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 the `Reordable` feature, not its place in the TreeList 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 -### Save and Load Grid State from Browser LocalStorage +You can find the following examples in this section: -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. +* [Save and Load TreeList State from Browser LocalStorage](#save-and-load-treelist-state-from-browser-localstorage) +* [Set TreeList Options Through State](#set-treelist-options-through-state) +* [Set Default (Initial) State](#set-default-initial-state) +* [Get and Override User Action That Changes The TreeList](#get-and-override-user-action-that-changes-the-treelist) +* [Initiate Editing or Inserting of an Item](#initiate-editing-or-inserting-of-an-item) ->caption Save, Load, Reset grid state on every state change. Uses a sample LocalStorage in the browser. +### Save and Load TreeList State from Browser LocalStorage + +The following example shows one way you can store the TreeList 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. + +>caption Save, Load, Reset the state of the TreeList on every state change. Uses a sample LocalStorage in the browser. ````Component @inject LocalStorage LocalStorage @@ -283,312 +299,186 @@ public class LocalStorage } ```` -### Set Grid Options Through State +### Set TreeList 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 TreeList state allows you to control the behavior of the grid programmatically - you can, for example, set sorts, filteres and expand items. ->tip The individual tabs below show how you can use the state to programmatically set the grid filtering, sorting, grouping and other features. +>tip The individual tabs below show how you can use the state to programmatically set the TreeList filtering, sorting and other features. @[template](/_contentTemplates/grid/state.md#initial-state) - +````ExpandedItems +@[template](/_contentTemplates/treelist/state.md#expand-items-from-code) +```` ````Sorting -@[template](/_contentTemplates/grid/state.md#set-sort-from-code) +@[template](/_contentTemplates/treelist/state.md#set-sort-from-code) ```` ````FilterRow -@[template](/_contentTemplates/grid/state.md#filter-row-from-code) +@[template](/_contentTemplates/treelist/state.md#filter-row-from-code) ```` ````FilterMenu -@[template](/_contentTemplates/grid/state.md#filter-menu-from-code) -```` -````Grouping -@[template](/_contentTemplates/grid/state.md#group-from-code) -```` -````Hierarchy -@[template](/_contentTemplates/grid/state.md#expand-hierarchy-from-code) +@[template](/_contentTemplates/treelist/state.md#filter-menu-from-code) ```` + ### Set Default (Initial) State -If you want the grid to start with certain settings for your end users, you can pre-define them in the `OnStateInit event`. +If you want the TreeList to start with certain settings for your end users, you can pre-define them in the `OnStateInit event`. ->caption Choose a default state of the grid for your users +>caption Choose a default state of the TreeList for your users ````CSHTML -@* 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; - - + + + + + + + + @code { - async Task OnStateInitHandler(GridStateEventArgs args) + async Task OnStateInitHandler(TreeListStateEventArgs args) { - var state = new GridState + var initialState = new TreeListState() { - SortDescriptors = new List + FilterDescriptors = new List() { - new Telerik.DataSource.SortDescriptor{ Member = "Name", SortDirection = Telerik.DataSource.ListSortDirection.Descending } + new FilterDescriptor() + { + Member = nameof(Employee.Name), + MemberType = typeof(string), + Operator = FilterOperator.Contains, + Value = "second level" + } }, - FilterDescriptors = new List() + SortDescriptors = new List() { - new Telerik.DataSource.FilterDescriptor() { Member = "Id", Operator = Telerik.DataSource.FilterOperator.IsLessThan, Value = 5, MemberType = typeof(int) }, - } + new SortDescriptor() + { + Member = nameof(Employee.Id), + SortDirection = ListSortDirection.Descending + } + }, + Page = 2 }; - args.GridState = state; + args.TreeListState = initialState; } - public IEnumerable 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 List Data { get; set; } - public class SampleData + // sample model + + public class Employee { + // hierarchical data collections + public List DirectReports { get; set; } + + // data fields for display public int Id { get; set; } public string Name { get; set; } - public string Team { get; set; } + public string EmailAddress { 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`]({%slug components/grid/manual-operations%}#cache-data-request) event, cache the previous `DataSourceRequest`, compare against it, alter it if needed, and implement the operations yourself. Another is to use the `OnStateChanged` event. + // data generation -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]({%slug grid-kb-static-group%}) Knowledge Base article. + // used in this example for data generation and retrieval for CUD operations on the current view-model data + public int LastId { get; set; } = 1; ->caption Know when the grid state changes, which parameter changes, and amend the change - -````CSHTML -@* 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 - - - - -@code { - TelerikGrid GridRef { get; set; } + protected override async Task OnInitializedAsync() + { + Data = await GetTreeListData(); + } - // 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 .SetState() call redraws only the grid, but not the rest of the page - async void OnStateChangedHandler(GridStateEventArgs args) + async Task> GetTreeListData() { - Console.WriteLine(args.PropertyName); // get the setting that was just changed (paging, sorting,...) + List data = new List(); - if (args.PropertyName == "FilterDescriptors") // sorting changed for our example + for (int i = 1; i < 15; i++) { - // 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 (FilterDescriptor item in args.GridState.FilterDescriptors) + Employee root = new Employee { - if(item.Member == "Id") + Id = LastId, + Name = $"root: {i}", + EmailAddress = $"{i}@example.com", + HireDate = DateTime.Now.AddYears(-i), + DirectReports = new List(), // prepare a collection for the child items, will be populated later in the code + }; + data.Add(root); + LastId++; + + for (int j = 1; j < 4; j++) + { + int currId = LastId; + Employee firstLevelChild = new Employee { - isIdFiltered = true; - } - - // you could override a user action as well - change settings on the corresponding parameter - // make sure that the .SetState() method of the grid is always called if you do that - if(item.Member == "Name") + Id = currId, + Name = $"first level child {j} of {i}", + EmailAddress = $"{currId}@example.com", + HireDate = DateTime.Now.AddDays(-currId), + DirectReports = new List(), // collection for child nodes + }; + root.DirectReports.Add(firstLevelChild); // populate the parent's collection + LastId++; + + for (int k = 1; k < 3; k++) { - item.Value = "name 1"; - item.Operator = FilterOperator.Contains; + int nestedId = LastId; + // populate the parent's collection + firstLevelChild.DirectReports.Add(new Employee + { + Id = LastId, + Name = $"second level child {k} of {j} and {i}", + EmailAddress = $"{nestedId}@example.com", + HireDate = DateTime.Now.AddMinutes(-nestedId) + }); ; + LastId++; } } - if (!isIdFiltered) - { - args.GridState.FilterDescriptors.Add(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.SetState(args.GridState); } - } - - public IEnumerable MyData = 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; } + return await Task.FromResult(data); } } ```` -### Initiate Editing or Inserting of an Item +### Get and Override User Action That Changes The TreeList -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. +Sometimes you may want to know what the user changed in the TreeList (e.g., when they filter, sort and so on) and even override those operations. -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]({%slug components/grid/columns/command%}). +The example below shows how to achieve it by using the`OnStateChanged` event. Review the code comments to see how it works and to make sure you don't get issues. ->caption Put and item in Edit mode or start Inserting a new item +>caption Know when the TreeList state changes, which parameter changes, and amend the change ````CSHTML -@* This example shows how to make the grid edit a certain item or start insert operation - through your own code, without requiring the user to click the Command buttons. - The buttons that initiate these operations can be anywhere on the page, inlcuding inside the grid. - Note the model constructors and static method that show how to get a new instance for the edit item - *@ - -Start Insert operation -Put item 4 in Edit mode - - - - - - Update - Edit - Delete - Cancel - - - - -@code { - TelerikGrid 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.SetState(currState); - } - - 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 itemToEdit = SampleData.GetClonedInstance(MyData.Where(itm => itm.ID == 4).FirstOrDefault()); - // you can alter values here as well (not mandatory) - //itemToEdit.Name = "Changed from code"; - currState.OriginalEditItem = itemToEdit; - // for InCell editing, you can use the EditField property instead - await GridRef.SetState(currState); - } - - - // Sample CRUD operations and data follow - - async Task UpdateHandler(GridCommandEventArgs args) - { - SampleData item = (SampleData)args.Item; - var index = MyData.FindIndex(i => i.ID == item.ID); - if (index != -1) - { - MyData[index] = item; - } - } - - async Task DeleteHandler(GridCommandEventArgs args) - { - SampleData item = (SampleData)args.Item; - MyData.Remove(item); - } - - async Task CreateHandler(GridCommandEventArgs args) - { - SampleData item = (SampleData)args.Item; - item.ID = MyData.Count + 1; - MyData.Insert(0, item); - - } - - 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() - { +```` - } +### Initiate Editing or Inserting of an Item - public SampleData(SampleData itmToClone) - { - this.ID = itmToClone.ID; - this.Name = itmToClone.Name; - } +The TreeList 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. - public static SampleData GetClonedInstance(SampleData itmToClone) - { - return new SampleData(itmToClone); - } - } +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]({%treelist-columns-command%}). - public List MyData { get; set; } +>caption Put and item in Edit mode or start Inserting a new item - protected override void OnInitialized() - { - MyData = new List(); +````CSHTML - for (int i = 0; i < 50; i++) - { - MyData.Add(new SampleData() - { - ID = i, - Name = "Name " + i.ToString() - }); - } - } -} ```` + ## See Also * [Live Demo: Grid State](https://demos.telerik.com/blazor-ui/grid/persist-state) From cd95c4174b8596ce69a713a7964636e833b1738d Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Thu, 15 Oct 2020 16:20:32 +0300 Subject: [PATCH 03/19] chore(treelist): state article docs --- components/treelist/state.md | 334 +++++++++++++---------------------- 1 file changed, 127 insertions(+), 207 deletions(-) diff --git a/components/treelist/state.md b/components/treelist/state.md index 08d0add1d5..08811f5ac4 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -21,7 +21,6 @@ This article contains the following sections: * [Methods](#methods) * [Information in the TreeList State](#information-in-the-treelist-state) * [Examples](#examples) - * [Save and Load TreeList State from Browser LocalStorage](#save-and-load-treelist-state-from-browser-localstorage) * [Set TreeList Options Through State](#set-treelist-options-through-state) * [Set Default (Initial) State](#set-default-initial-state) * [Get and Override User Action That Changes The TreeList](#get-and-override-user-action-that-changes-the-treelist) @@ -92,212 +91,11 @@ The following information is present in the TreeList state: You can find the following examples in this section: -* [Save and Load TreeList State from Browser LocalStorage](#save-and-load-treelist-state-from-browser-localstorage) * [Set TreeList Options Through State](#set-treelist-options-through-state) * [Set Default (Initial) State](#set-default-initial-state) * [Get and Override User Action That Changes The TreeList](#get-and-override-user-action-that-changes-the-treelist) * [Initiate Editing or Inserting of an Item](#initiate-editing-or-inserting-of-an-item) -### Save and Load TreeList State from Browser LocalStorage - -The following example shows one way you can store the TreeList 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. - ->caption Save, Load, Reset the state of the TreeList on every state change. Uses a sample LocalStorage in the browser. - -````Component -@inject LocalStorage LocalStorage -@inject IJSRuntime JsInterop -@using Telerik.DataSource; - -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. -
- -Reload the page to see the current grid state preserved -Reset the state - - - - - - - - Edit - Delete - Save - Cancel - - - - Add Employee - - - -@if (SelectedItems != null) -{ -
    - @foreach (SampleData employee in SelectedItems) - { -
  • - @employee.Id -
  • - } -
-} - -@code { - // Load and Save the state through the grid events - - string UniqueStorageKey = "SampleGridStateStorageThatShouldBeUnique"; - - async Task OnStateInitHandler(GridStateEventArgs args) - { - try - { - var state = await LocalStorage.GetItem>(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 args) - { - await LocalStorage.SetItem(UniqueStorageKey, args.GridState); - } - - TelerikGrid Grid { get; set; } - async Task ResetState() - { - // clean up the storage - await LocalStorage.RemoveItem(UniqueStorageKey); - - await Grid.SetState(null); // pass null to reset the state - } - - void ReloadPage() - { - JsInterop.InvokeVoidAsync("window.location.reload"); - } - - // Sample CRUD operations - - private void CreateItem(GridCommandEventArgs args) - { - var argsItem = args.Item as SampleData; - - argsItem.Id = GridData.Count + 1; - - GridData.Insert(0, argsItem); - } - - private void DeleteItem(GridCommandEventArgs args) - { - var argsItem = args.Item as SampleData; - - GridData.Remove(argsItem); - } - - private void UpdateItem(GridCommandEventArgs args) - { - var argsItem = args.Item as SampleData; - var index = GridData.FindIndex(i => i.Id == argsItem.Id); - if (index != -1) - { - GridData[index] = argsItem; - } - } - - // Sample data follows below - - public IEnumerable SelectedItems { get; set; } = Enumerable.Empty(); - - public List GridData { get; set; } = Enumerable.Range(1, 30).Select(x => new SampleData - { - Id = x, - Name = "name " + x, - Team = "team " + x % 5 - }).ToList(); - - public class SampleData - { - public int Id { get; set; } - public string Name { get; set; } - public string Team { 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; - } - } -} -```` -````Service -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 GetItem(string key) - { - var data = await JSRuntimeInstance.InvokeAsync("localStorage.getItem", key); - if (!string.IsNullOrEmpty(data)) - { - return JsonSerializer.Deserialize(data); - } - - return default; - } - - public ValueTask RemoveItem(string key) - { - return JSRuntimeInstance.InvokeVoidAsync("localStorage.removeItem", key); - } -} -```` ### Set TreeList Options Through State @@ -454,16 +252,138 @@ If you want the TreeList to start with certain settings for your end users, you } ```` -### Get and Override User Action That Changes The TreeList +### Get User Action That Changes The TreeList -Sometimes you may want to know what the user changed in the TreeList (e.g., when they filter, sort and so on) and even override those operations. +Sometimes you may want to know what the user changed in the TreeList (e.g., when they filter, sort and so on). -The example below shows how to achieve it by using the`OnStateChanged` event. Review the code comments to see how it works and to make sure you don't get issues. +The example below shows how to achieve it by using the`OnStateChanged` event. ->caption Know when the TreeList state changes, which parameter changes, and amend the change +>caption Know when the TreeList state changes and which parameter changed ````CSHTML +@using Telerik.DataSource; + + + + + + + + + +@Result + +@code { + TelerikTreeList TreeListRef { get; set; } = new TelerikTreeList(); + + public string Result { get; set; } + + async Task OnStateChangedHandler(TreeListStateEventArgs args) + { + string changedSetting = args.PropertyName; + + if(changedSetting == "SortDescriptors") + { + foreach (var item in args.TreeListState.SortDescriptors) + { + Result = $"The {item.Member} field was sorted"; + } + } + else if(changedSetting == "FilterDescriptors") + { + foreach (FilterDescriptor item in args.TreeListState.FilterDescriptors) + { + Result = $"The {item.Member} field was filtered"; + } + } + } + + public List Data { get; set; } + + // sample model + + public class Employee + { + // hierarchical data collections + public List DirectReports { get; set; } + + // data fields for display + public int Id { get; set; } + public string Name { get; set; } + public string EmailAddress { get; set; } + public DateTime HireDate { get; set; } + } + + // data generation + + // used in this example for data generation and retrieval for CUD operations on the current view-model data + public int LastId { get; set; } = 1; + + protected override async Task OnInitializedAsync() + { + Data = await GetTreeListData(); + } + + async Task> GetTreeListData() + { + List data = new List(); + + for (int i = 1; i < 15; i++) + { + Employee root = new Employee + { + Id = LastId, + Name = $"root: {i}", + EmailAddress = $"{i}@example.com", + HireDate = DateTime.Now.AddYears(-i), + DirectReports = new List(), // prepare a collection for the child items, will be populated later in the code + }; + data.Add(root); + LastId++; + + for (int j = 1; j < 4; j++) + { + int currId = LastId; + Employee firstLevelChild = new Employee + { + Id = currId, + Name = $"first level child {j} of {i}", + EmailAddress = $"{currId}@example.com", + HireDate = DateTime.Now.AddDays(-currId), + DirectReports = new List(), // collection for child nodes + }; + root.DirectReports.Add(firstLevelChild); // populate the parent's collection + LastId++; + + for (int k = 1; k < 3; k++) + { + int nestedId = LastId; + // populate the parent's collection + firstLevelChild.DirectReports.Add(new Employee + { + Id = LastId, + Name = $"second level child {k} of {j} and {i}", + EmailAddress = $"{nestedId}@example.com", + HireDate = DateTime.Now.AddMinutes(-nestedId) + }); ; + LastId++; + } + } + } + + return await Task.FromResult(data); + } +} ```` ### Initiate Editing or Inserting of an Item @@ -481,5 +401,5 @@ In addition to that, you can also use the `EditItem`, `OriginalEditItem` and `In ## See Also - * [Live Demo: Grid State](https://demos.telerik.com/blazor-ui/grid/persist-state) + * [Live Demo: TreeList State](https://demos.telerik.com/blazor-ui/treelist/persist-state) From dcbea026c4b95f5a2a6ac706240be95417125fdf Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Thu, 15 Oct 2020 16:24:17 +0300 Subject: [PATCH 04/19] chore(treelist): publish article --- components/treelist/state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/treelist/state.md b/components/treelist/state.md index 08811f5ac4..f90e83a688 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -4,7 +4,7 @@ page_title: TreeList - State description: Save, load, change the treelist for Blazor state - sorting, filtering and so on. slug: treelist-state tags: telerik,blazor,treelist,state,save,load,layout,set,change,management -published: false +published: true position: 50 --- From 781e41689f889ad08e7dbff6a662adbb67b34f59 Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Thu, 15 Oct 2020 16:27:47 +0300 Subject: [PATCH 05/19] chore(treelist): fix slug --- components/treelist/state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/treelist/state.md b/components/treelist/state.md index f90e83a688..e8e651a9f5 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -390,7 +390,7 @@ The example below shows how to achieve it by using the`OnStateChanged` event. The TreeList 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]({%treelist-columns-command%}). +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]({%slug treelist-columns-command%}). >caption Put and item in Edit mode or start Inserting a new item From 29a7f79cd5b6ecc75f5a395be644b639be06cb4c Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Thu, 15 Oct 2020 18:06:30 +0300 Subject: [PATCH 06/19] chore(treelist): add new example to state article --- components/treelist/state.md | 255 ++++++++++++++++++++++++++++++++++- 1 file changed, 252 insertions(+), 3 deletions(-) diff --git a/components/treelist/state.md b/components/treelist/state.md index e8e651a9f5..bff7bcaac8 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -23,7 +23,7 @@ This article contains the following sections: * [Examples](#examples) * [Set TreeList Options Through State](#set-treelist-options-through-state) * [Set Default (Initial) State](#set-default-initial-state) - * [Get and Override User Action That Changes The TreeList](#get-and-override-user-action-that-changes-the-treelist) + * [Get The User Action That Changes The TreeList](#get-the-user-action-that-changes-the-treelist) * [Initiate Editing or Inserting of an Item](#initiate-editing-or-inserting-of-an-item) @@ -93,7 +93,7 @@ You can find the following examples in this section: * [Set TreeList Options Through State](#set-treelist-options-through-state) * [Set Default (Initial) State](#set-default-initial-state) -* [Get and Override User Action That Changes The TreeList](#get-and-override-user-action-that-changes-the-treelist) +* [Get The User Action That Changes The TreeList](#get-the-user-action-that-changes-the-treelist) * [Initiate Editing or Inserting of an Item](#initiate-editing-or-inserting-of-an-item) @@ -252,7 +252,7 @@ If you want the TreeList to start with certain settings for your end users, you } ```` -### Get User Action That Changes The TreeList +### Get The User Action That Changes The TreeList Sometimes you may want to know what the user changed in the TreeList (e.g., when they filter, sort and so on). @@ -395,7 +395,256 @@ In addition to that, you can also use the `EditItem`, `OriginalEditItem` and `In >caption Put and item in Edit mode or start Inserting a new item ````CSHTML +@using Telerik.DataSource; + +Edit item 3 +Insert Item + + + + + Add + + + + Add Child + Edit + Delete + Update + Cancel + + + + + + + + + +@code { + TelerikTreeList TreeListRef { get; set; } = new TelerikTreeList(); + + async Task EnterEditMode() + { + var state = TreeListRef.GetState(); + + Employee employeeToEdit = Employee.GetClonedInstance(FindItemRecursive(Data, 3)); + state.OriginalEditItem = employeeToEdit; + await TreeListRef.SetState(state); + } + + async Task InsertItem() + { + var state = TreeListRef.GetState(); + state.InsertedItem = new Employee() { Name = "added from code" }; + await TreeListRef.SetState(state); + } + + public List Data { get; set; } + + // used in this example for data generation and retrieval for CUD operations on the current view-model data + public int LastId { get; set; } = 1; + + // Sample CUD operations for the local data + async Task UpdateItem(TreeListCommandEventArgs args) + { + var item = args.Item as Employee; + + var foundItem = FindItemRecursive(Data, item.Id); + if (foundItem != null) + { + foundItem.Name = item.Name; + foundItem.HireDate = item.HireDate; + foundItem.EmailAddress = item.EmailAddress; + } + } + + async Task CreateItem(TreeListCommandEventArgs args) + { + var argsItem = args.Item as Employee; + + argsItem.Id = LastId++; + + if (args.ParentItem != null) + { + var parent = (Employee)args.ParentItem; + + parent.HasChildren = true; + if (parent.DirectReports == null) + { + parent.DirectReports = new List(); + } + + parent.DirectReports.Insert(0, argsItem); + } + else + { + Data.Insert(0, argsItem); + } + } + + async Task DeleteItem(TreeListCommandEventArgs args) + { + var item = args.Item as Employee; + + RemoveChildRecursive(Data, item); + } + + // sample helper methods for handling the view-model data hierarchy + + private Employee FindItemRecursive(List items, int id) + { + foreach (var item in items) + { + if (item.Id.Equals(id)) + { + return item; + } + + if (item.DirectReports?.Count > 0) + { + var childItem = FindItemRecursive(item.DirectReports, id); + + if (childItem != null) + { + return childItem; + } + } + } + + return null; + } + + private void RemoveChildRecursive(List items, Employee item) + { + for (int i = 0; i < items.Count(); i++) + { + if (item.Equals(items[i])) + { + items.Remove(item); + + return; + } + else if (items[i].DirectReports?.Count > 0) + { + RemoveChildRecursive(items[i].DirectReports, item); + + if (items[i].DirectReports.Count == 0) + { + items[i].HasChildren = false; + } + } + } + } + + // sample model + + public class Employee + { + public int Id { get; set; } + + public string Name { get; set; } + public string EmailAddress { get; set; } + public DateTime HireDate { get; set; } + + public List DirectReports { get; set; } + public bool HasChildren { get; set; } + + public override bool Equals(object obj) + { + if (obj is Employee) + { + return this.Id == (obj as Employee).Id; + } + return false; + } + + public Employee() + { + + } + + public Employee(Employee itmToClone) + { + this.Id = itmToClone.Id; + this.Name = itmToClone.Name; + } + + public static Employee GetClonedInstance(Employee itmToClone) + { + return new Employee(itmToClone); + } + } + + // data generation + + protected override async Task OnInitializedAsync() + { + Data = await GetTreeListData(); + } + + async Task> GetTreeListData() + { + List data = new List(); + + for (int i = 1; i < 15; i++) + { + Employee root = new Employee + { + Id = LastId, + Name = $"root: {i}", + EmailAddress = $"{i}@example.com", + HireDate = DateTime.Now.AddYears(-i), + DirectReports = new List(), + HasChildren = true + }; + data.Add(root); + LastId++; + + for (int j = 1; j < 4; j++) + { + int currId = LastId; + Employee firstLevelChild = new Employee + { + Id = currId, + Name = $"first level child {j} of {i}", + EmailAddress = $"{currId}@example.com", + HireDate = DateTime.Now.AddDays(-currId), + DirectReports = new List(), + HasChildren = true + }; + root.DirectReports.Add(firstLevelChild); + LastId++; + + for (int k = 1; k < 3; k++) + { + int nestedId = LastId; + firstLevelChild.DirectReports.Add(new Employee + { + Id = LastId, + Name = $"second level child {k} of {j} and {i}", + EmailAddress = $"{nestedId}@example.com", + HireDate = DateTime.Now.AddMinutes(-nestedId) + }); ; + LastId++; + } + } + } + + data[0].Name += " (non-editable, see OnEdit)"; + + return await Task.FromResult(data); + } +} ```` From af943693d74b5f2b2d33486da83f2e3cd676be01 Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Fri, 16 Oct 2020 12:57:26 +0300 Subject: [PATCH 07/19] chore(treelist): add example on how to load / save state to LocalStorage --- components/treelist/state.md | 224 +++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) diff --git a/components/treelist/state.md b/components/treelist/state.md index bff7bcaac8..6818179e69 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -21,6 +21,7 @@ This article contains the following sections: * [Methods](#methods) * [Information in the TreeList State](#information-in-the-treelist-state) * [Examples](#examples) + * * [Save and Load TreeList State from Browser LocalStorage](#save-and-load-treelist-state-from-browser-localstorage) * [Set TreeList Options Through State](#set-treelist-options-through-state) * [Set Default (Initial) State](#set-default-initial-state) * [Get The User Action That Changes The TreeList](#get-the-user-action-that-changes-the-treelist) @@ -91,11 +92,234 @@ The following information is present in the TreeList state: You can find the following examples in this section: +* [Save and Load TreeList State from Browser LocalStorage](#save-and-load-treelist-state-from-browser-localstorage) * [Set TreeList Options Through State](#set-treelist-options-through-state) * [Set Default (Initial) State](#set-default-initial-state) * [Get The User Action That Changes The TreeList](#get-the-user-action-that-changes-the-treelist) * [Initiate Editing or Inserting of an Item](#initiate-editing-or-inserting-of-an-item) +### Save and Load TreeList State from Browser LocalStorage + +The following example shows one way you can store the TreeList 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. + +>note If you use Hierarchical data for the TreeList you need to serialize the current item only and not the entire collection in order not to exceed the size of the LocalStorage. + +>caption Save, Load, Reset TreeList state on every state change. Uses a sample LocalStorage in the browser. + +````Component +@inject IJSRuntime JsInterop +@using Telerik.DataSource; + +@inject LocalStorage LocalStorage + +Reload the page to see the current TreeList state preserved +Reset the state +Set the state + + + + + + + + + + + + +@code { + string UniqueStorageKey = "SampleTreeListStateStorageKey"; + + async Task OnStateInitHandler(TreeListStateEventArgs args) + { + args.TreeListState = await LocalStorage.GetItem>(UniqueStorageKey); + } + + async Task OnStateChangedHandler(TreeListStateEventArgs args) + { + var state = args.TreeListState; + state.ExpandedItems = null; + await LocalStorage.SetItem(UniqueStorageKey, state); + } + + async Task ResetState() + { + // clean up the storage + await LocalStorage.RemoveItem(UniqueStorageKey); + + await TreeListRef.SetState(null); // pass null to reset the state + } + + void ReloadPage() + { + JsInterop.InvokeVoidAsync("window.location.reload"); + } + + private void SetState() + { + TreeListState state = new TreeListState() + { + FilterDescriptors = new List() + { + new FilterDescriptor() { Member="StringProp", MemberType=typeof(string), Value = "2", Operator = FilterOperator.Contains } + }, + SortDescriptors = new List() + { + new SortDescriptor() { Member = "StringProp", SortDirection = ListSortDirection.Descending } + }, + Page = 2, + ColumnStates = new List() + { + new TreeListColumnState() + { + Index = 3, + Width = "150px" + }, + new TreeListColumnState() + { + Index = 1, + Width = "120px" + }, + new TreeListColumnState() + { + Index = 2, + Width = "60px" + }, + new TreeListColumnState() + { + Index = 4, + Width = "150px" + }, + new TreeListColumnState() + { + Index = 0, + Width = "120px" + } + } + }; + + TreeListRef?.SetState(state); + } + + TelerikTreeList TreeListRef { get; set; } + + public List Data { get; set; } + + protected override async Task OnInitializedAsync() + { + Data = await GetTreeListData(); + } + + // sample model + + public class Employee + { + // denote the parent-child relationship between items + public int Id { get; set; } + public int? ParentId { get; set; } + + // custom data fields for display + public string Name { get; set; } + public string EmailAddress { get; set; } + public DateTime HireDate { get; set; } + } + + // data generation + + async Task> GetTreeListData() + { + List data = new List(); + + for (int i = 1; i < 15; i++) + { + data.Add(new Employee + { + Id = i, + ParentId = null, // indicates a root-level item + Name = $"root: {i}", + EmailAddress = $"{i}@example.com", + HireDate = DateTime.Now.AddYears(-i) + }); ; + + for (int j = 1; j < 4; j++) + { + int currId = i * 100 + j; + data.Add(new Employee + { + Id = currId, + ParentId = i, + Name = $"first level child {j} of {i}", + EmailAddress = $"{currId}@example.com", + HireDate = DateTime.Now.AddDays(-currId) + }); + + for (int k = 1; k < 3; k++) + { + int nestedId = currId * 1000 + k; + data.Add(new Employee + { + Id = nestedId, + ParentId = currId, + Name = $"second level child {k} of {i} and {currId}", + EmailAddress = $"{nestedId}@example.com", + HireDate = DateTime.Now.AddMinutes(-nestedId) + }); ; + } + } + } + + return await Task.FromResult(data); + } +} +```` +````Service +using System.Threading.Tasks; +using Microsoft.JSInterop; +using System.Text.Json; + +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 GetItem(string key) + { + var data = await JSRuntimeInstance.InvokeAsync("localStorage.getItem", key); + if (!string.IsNullOrEmpty(data)) + { + return JsonSerializer.Deserialize(data); + } + + return default; + } + + public ValueTask RemoveItem(string key) + { + return JSRuntimeInstance.InvokeVoidAsync("localStorage.removeItem", key); + } +} +```` ### Set TreeList Options Through State From 5f54293e6b3cc55fc5e2144d7d9b1b1db12ffd3a Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Fri, 16 Oct 2020 12:58:30 +0300 Subject: [PATCH 08/19] chore(treelist): wording --- components/treelist/state.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/treelist/state.md b/components/treelist/state.md index 6818179e69..b64cada4be 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -71,7 +71,7 @@ The following information is present in the TreeList 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 TreeList - the data is updated in the `OnChange` event, not on every keystroke for performance reasons). The `OriginalEditItem` 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). +* **Filtering** - filter descriptors (fields by which the Treelist is filtered, the operator and value). * **Paging** - page index @@ -323,11 +323,11 @@ public class LocalStorage ### Set TreeList Options Through State -The TreeList state allows you to control the behavior of the grid programmatically - you can, for example, set sorts, filteres and expand items. +The TreeList state allows you to control the behavior of the TreeList programmatically - you can, for example, set sorts, filteres and expand items. >tip The individual tabs below show how you can use the state to programmatically set the TreeList filtering, sorting and other features. -@[template](/_contentTemplates/grid/state.md#initial-state) +@[template](/_contentTemplates/treelist/state.md#initial-state) ````ExpandedItems @[template](/_contentTemplates/treelist/state.md#expand-items-from-code) @@ -612,9 +612,9 @@ The example below shows how to achieve it by using the`OnStateChanged` event. ### Initiate Editing or Inserting of an Item -The TreeList 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. +The TreeList 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 TreeList 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]({%slug treelist-columns-command%}). +In addition to that, you can also use the `EditItem`, `OriginalEditItem` and `InsertItem` fields of the state object to put the TreeList in edit/insert mode through your own application code, instead of needing the user to initiate this through a [command button]({%slug treelist-columns-command%}). >caption Put and item in Edit mode or start Inserting a new item From 136aa7429d75317b096fbcbc4afd97f942005fc1 Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Fri, 16 Oct 2020 13:02:56 +0300 Subject: [PATCH 09/19] chore(treelist): fix ToC --- components/treelist/state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/treelist/state.md b/components/treelist/state.md index b64cada4be..f9e3c170fa 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -21,7 +21,7 @@ This article contains the following sections: * [Methods](#methods) * [Information in the TreeList State](#information-in-the-treelist-state) * [Examples](#examples) - * * [Save and Load TreeList State from Browser LocalStorage](#save-and-load-treelist-state-from-browser-localstorage) + * [Save and Load TreeList State from Browser LocalStorage](#save-and-load-treelist-state-from-browser-localstorage) * [Set TreeList Options Through State](#set-treelist-options-through-state) * [Set Default (Initial) State](#set-default-initial-state) * [Get The User Action That Changes The TreeList](#get-the-user-action-that-changes-the-treelist) From 6d6b96370266efa7dd93ae230a1fd9154a6436ef Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Mon, 19 Oct 2020 13:31:43 +0300 Subject: [PATCH 10/19] chore(treelist): link hierarchical data article and clarify the note --- components/treelist/state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/treelist/state.md b/components/treelist/state.md index f9e3c170fa..471d80a37d 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -102,7 +102,7 @@ You can find the following examples in this section: The following example shows one way you can store the TreeList 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. ->note If you use Hierarchical data for the TreeList you need to serialize the current item only and not the entire collection in order not to exceed the size of the LocalStorage. +>note If you use [Hierarchical data]({%slug treelist-data-binding-hierarchical-data%}) for the TreeList you need to serialize the current item only and not the entire collection of child items in order not to exceed the size of the LocalStorage. >caption Save, Load, Reset TreeList state on every state change. Uses a sample LocalStorage in the browser. From e1572f6a57770096bef800d356bf75e7f9bde133 Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Mon, 19 Oct 2020 13:33:20 +0300 Subject: [PATCH 11/19] chore(treelist): reorder inject directives --- components/treelist/state.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/components/treelist/state.md b/components/treelist/state.md index 471d80a37d..7ebc5d1481 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -107,10 +107,10 @@ The following example shows one way you can store the TreeList state - through a >caption Save, Load, Reset TreeList state on every state change. Uses a sample LocalStorage in the browser. ````Component -@inject IJSRuntime JsInterop @using Telerik.DataSource; @inject LocalStorage LocalStorage +@inject IJSRuntime JsInterop Reload the page to see the current TreeList state preserved Reset the state @@ -119,7 +119,7 @@ The following example shows one way you can store the TreeList state - through a -@code { - string UniqueStorageKey = "SampleTreeListStateStorageKey"; +@code { string UniqueStorageKey = "SampleTreeListStateStorageKey"; async Task OnStateInitHandler(TreeListStateEventArgs args) { @@ -167,16 +166,16 @@ The following example shows one way you can store the TreeList state - through a TreeListState state = new TreeListState() { FilterDescriptors = new List() - { + { new FilterDescriptor() { Member="StringProp", MemberType=typeof(string), Value = "2", Operator = FilterOperator.Contains } }, SortDescriptors = new List() - { + { new SortDescriptor() { Member = "StringProp", SortDirection = ListSortDirection.Descending } }, Page = 2, ColumnStates = new List() - { + { new TreeListColumnState() { Index = 3, From 0243696e6384312e0d47b81efc4e5349027be597 Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Mon, 19 Oct 2020 13:36:04 +0300 Subject: [PATCH 12/19] chore(treelist): fix curly brackets indentation --- components/treelist/state.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/treelist/state.md b/components/treelist/state.md index 7ebc5d1481..9e2eabb3e8 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -166,16 +166,16 @@ The following example shows one way you can store the TreeList state - through a TreeListState state = new TreeListState() { FilterDescriptors = new List() - { + { new FilterDescriptor() { Member="StringProp", MemberType=typeof(string), Value = "2", Operator = FilterOperator.Contains } }, SortDescriptors = new List() - { + { new SortDescriptor() { Member = "StringProp", SortDirection = ListSortDirection.Descending } }, Page = 2, ColumnStates = new List() - { + { new TreeListColumnState() { Index = 3, From f66a4300e1265965f35320ce78129b3eda3b6fc4 Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Mon, 19 Oct 2020 13:36:44 +0300 Subject: [PATCH 13/19] chore(treelist): fix typo in Set option through state example --- components/treelist/state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/treelist/state.md b/components/treelist/state.md index 9e2eabb3e8..4f93b1bd6b 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -322,7 +322,7 @@ public class LocalStorage ### Set TreeList Options Through State -The TreeList state allows you to control the behavior of the TreeList programmatically - you can, for example, set sorts, filteres and expand items. +The TreeList state allows you to control the behavior of the TreeList programmatically - you can, for example, set sorts, filters and expand items. >tip The individual tabs below show how you can use the state to programmatically set the TreeList filtering, sorting and other features. From edd49ef6796f01a6896d59a414d4b45cd477557d Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Mon, 19 Oct 2020 13:40:46 +0300 Subject: [PATCH 14/19] chore(treelist): add filtering from code section in the filtering article --- components/treelist/filtering.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/components/treelist/filtering.md b/components/treelist/filtering.md index d69b4a875e..fdba6404ca 100644 --- a/components/treelist/filtering.md +++ b/components/treelist/filtering.md @@ -410,6 +410,21 @@ The `TreeListSearchBox` component offers the following settings to customize its } ```` +### Filtering From Code + +You can set the TreeList filters from your code through the grid [state]({%slug treelist-state%}). + +@[template](/_contentTemplates/treelist/state.md#initial-state) + +>caption Set sorting programmatically + +````FilterRow +@[template](/_contentTemplates/treelist/state.md#filter-row-from-code) +```` +````FilterMenu +@[template](/_contentTemplates/treelist/state.md#filter-menu-from-code) +```` + ## See Also From 901d5439983eea53103a35df6b0d6c13949db523 Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Mon, 19 Oct 2020 13:43:28 +0300 Subject: [PATCH 15/19] chore(treelist): add sorting from code section in sorting article --- components/treelist/sorting.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/components/treelist/sorting.md b/components/treelist/sorting.md index ca26fa677f..c1c55bdad8 100644 --- a/components/treelist/sorting.md +++ b/components/treelist/sorting.md @@ -98,6 +98,16 @@ Click a column header to sort by its data ![](images/basic-sorting.png) +You can sort the TreeList from your own code through its [state]({%slug treelist-state%}). + +@[template](/_contentTemplates/treelist/state.md#initial-state) + +>caption Set sorting programmatically + +````CSHTML +@[template](/_contentTemplates/treelist/state.md#set-sort-from-code) +```` + ## See Also From 03966703d48d09cf044f1cff41b6a7eeae518d2e Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Mon, 19 Oct 2020 15:12:50 +0300 Subject: [PATCH 16/19] chore(treelist): improve expanded items example and add tip in onstateinit section --- _contentTemplates/treelist/state.md | 35 ++++++++++++++++++++++++----- components/treelist/state.md | 2 ++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/_contentTemplates/treelist/state.md b/_contentTemplates/treelist/state.md index e7d2c1e490..f108f81da5 100644 --- a/_contentTemplates/treelist/state.md +++ b/_contentTemplates/treelist/state.md @@ -388,11 +388,24 @@ #expand-items-from-code -@*Collapse all items but the first one*@ +@*Expand a root level item on a button click*@ @using Telerik.DataSource; -Expand the first item only +
+ + + + + + Expand the selected root level item + +
+ +
@@ -413,6 +427,17 @@ @code { public TelerikTreeList TreeListRef { get; set; } = new TelerikTreeList(); + public int ExpandedItemIndex { get; set; } + + async Task OnStateInitHandler(TreeListStateEventArgs args) + { + var collapsedItemsState = new TreeListState() + { + ExpandedItems = new List() + }; + + args.TreeListState = collapsedItemsState; + } async Task SetTreeListExpandedItems() { @@ -420,7 +445,7 @@ { ExpandedItems = new List() { - Data[0] + Data[ExpandedItemIndex] } }; diff --git a/components/treelist/state.md b/components/treelist/state.md index 4f93b1bd6b..73e198f98c 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -346,6 +346,8 @@ The TreeList state allows you to control the behavior of the TreeList programmat If you want the TreeList to start with certain settings for your end users, you can pre-define them in the `OnStateInit event`. +>tip The `ExpandedItems` sample in the [Set TreeList Options Through State](#set-treelist-options-through-state) section shows how to collapse all items in the OnStateInit event handler. + >caption Choose a default state of the TreeList for your users ````CSHTML From 1e052bea1c73ed05d1db8f57419a94d2a755d040 Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Mon, 19 Oct 2020 15:30:48 +0300 Subject: [PATCH 17/19] chore(treelist): add comment to expanded items --- _contentTemplates/treelist/state.md | 1 + 1 file changed, 1 insertion(+) diff --git a/_contentTemplates/treelist/state.md b/_contentTemplates/treelist/state.md index f108f81da5..3361b953c3 100644 --- a/_contentTemplates/treelist/state.md +++ b/_contentTemplates/treelist/state.md @@ -433,6 +433,7 @@ { var collapsedItemsState = new TreeListState() { + //collapse all items in the TreeList upon initialization of the state ExpandedItems = new List() }; From f4b5a2d7d3c604c71be219731635225f4245b90a Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Mon, 19 Oct 2020 16:37:10 +0300 Subject: [PATCH 18/19] chore(treelist): add column example --- _contentTemplates/treelist/state.md | 131 ++++++++++++++++++++++++++++ components/treelist/state.md | 8 ++ 2 files changed, 139 insertions(+) diff --git a/_contentTemplates/treelist/state.md b/_contentTemplates/treelist/state.md index 3361b953c3..fd393effb6 100644 --- a/_contentTemplates/treelist/state.md +++ b/_contentTemplates/treelist/state.md @@ -531,3 +531,134 @@ } #end +#get-column-state-from-code +@using Telerik.DataSource; + +Get the state of the Columns + +@( new MarkupString(Logger) ) + + + + + + + + + + +@code { + public TelerikTreeList TreeListRef { get; set; } = new TelerikTreeList(); + public List ColumnFields => new List + { + nameof(Employee.Name), + nameof(Employee.Id), + nameof(Employee.EmailAddress), + nameof(Employee.HireDate) + }; + public string Logger { get; set; } = String.Empty; + + public async Task GetColumnsFromState() + { + var columnsState = TreeListRef.GetState().ColumnStates; + + int index = 0; + + foreach (var item in columnsState) + { + string columnField = ColumnFields[index]; + + bool isVisible = item.Visible != false; + + string log = $"

Column: {columnField} | Index in TreeList: {item.Index} | Index in state: {index} | Visible: {isVisible} | Locked: {item.Locked}

"; + Logger += log; + index++; + } + } + + public List Data { get; set; } + + // sample model + + public class Employee + { + // hierarchical data collections + public List DirectReports { get; set; } + + // data fields for display + public int Id { get; set; } + public string Name { get; set; } + public string EmailAddress { get; set; } + public DateTime HireDate { get; set; } + } + + // data generation + + // used in this example for data generation and retrieval for CUD operations on the current view-model data + public int LastId { get; set; } = 1; + + protected override async Task OnInitializedAsync() + { + Data = await GetTreeListData(); + } + + async Task> GetTreeListData() + { + List data = new List(); + + for (int i = 1; i < 15; i++) + { + Employee root = new Employee + { + Id = LastId, + Name = $"root: {i}", + EmailAddress = $"{i}@example.com", + HireDate = DateTime.Now.AddYears(-i), + DirectReports = new List(), // prepare a collection for the child items, will be populated later in the code + }; + data.Add(root); + LastId++; + + for (int j = 1; j < 4; j++) + { + int currId = LastId; + Employee firstLevelChild = new Employee + { + Id = currId, + Name = $"first level child {j} of {i}", + EmailAddress = $"{currId}@example.com", + HireDate = DateTime.Now.AddDays(-currId), + DirectReports = new List(), // collection for child nodes + }; + root.DirectReports.Add(firstLevelChild); // populate the parent's collection + LastId++; + + for (int k = 1; k < 3; k++) + { + int nestedId = LastId; + // populate the parent's collection + firstLevelChild.DirectReports.Add(new Employee + { + Id = LastId, + Name = $"second level child {k} of {j} and {i}", + EmailAddress = $"{nestedId}@example.com", + HireDate = DateTime.Now.AddMinutes(-nestedId) + }); ; + LastId++; + } + } + } + + return await Task.FromResult(data); + } +} +#end + diff --git a/components/treelist/state.md b/components/treelist/state.md index 73e198f98c..e939ff3381 100644 --- a/components/treelist/state.md +++ b/components/treelist/state.md @@ -872,6 +872,14 @@ In addition to that, you can also use the `EditItem`, `OriginalEditItem` and `In } ```` +### Get Current Columns Visibility, Order, Field + +The `ColumnStates` field of the state object provides you with information about the current columns in the TreeList. The `Index` field describes the position the user chose, and the `Visible` parameter indicates whether the column is hidden or not. By looping over that collection you can know what the user sees. You could, for example, sort by the index and filter by the visibility of the columns to approximate the view of the user. + +````CSHTML +@[template](/_contentTemplates/treelist/state.md#get-column-state-from-code) +```` + ## See Also From 606ad3bd484df0d5670311240b1ae51f34cfb2b1 Mon Sep 17 00:00:00 2001 From: Svetoslav Dimitrov Date: Mon, 19 Oct 2020 17:05:09 +0300 Subject: [PATCH 19/19] chore(treelist): add relevant comments to column example --- _contentTemplates/treelist/state.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/_contentTemplates/treelist/state.md b/_contentTemplates/treelist/state.md index fd393effb6..8f5aabdd55 100644 --- a/_contentTemplates/treelist/state.md +++ b/_contentTemplates/treelist/state.md @@ -532,6 +532,9 @@ #end #get-column-state-from-code + +@* 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. This example also shows a workaround for getting the Field of the column that will be availale in a future release as part of the column state. *@ + @using Telerik.DataSource; Get the state of the Columns @@ -557,6 +560,8 @@ @code { public TelerikTreeList TreeListRef { get; set; } = new TelerikTreeList(); + + //part of workaround for getting the field too public List ColumnFields => new List { nameof(Employee.Name), @@ -568,6 +573,7 @@ public async Task GetColumnsFromState() { + // final part of the workaround for getting the field var columnsState = TreeListRef.GetState().ColumnStates; int index = 0;