TreeList State
The TreeList lets you read, save, load, and change its state through code. The state includes the TreeList features that are controlled by the user, such as the current sorting, page number, applied grouping, column widths, and many others.
This article describes:
- The properties of the
TreeListStateobject. - How to set initial TreeList configuration programmatically in
OnStateInit. - How to detect user changes in the TreeList state with
OnStateChanged. - How to use TreeList methods to get and set the TreeList state.
- Why you may need to override the
Equalsmethod of the TreeList model class.
Information in the TreeList State
The TreeList state is a generic class TreeListState<TItem>. The type depends on the type of the TreeList model. The TreeListState<TItem> object exposes the following properties:
| Property | Type | Description |
|---|---|---|
ColumnStates | ICollection<TreeListColumnState> | Information about each column's reorder index, width, visibility, locked state, Id parameter value and Field. The column order in the collection matches the column order in the TreeList declaration. On the other hand, the Index property matches the current column's position in the UI. Id and Field are always null after deserialization, because these properties have no public setters. |
EditField | string | The currently edited data item property in Incell edit mode. |
EditItem | TItem* | The currently edited data item in any edit mode. |
ExpandedItems | ICollection<TItem> | The expanded data items. |
FilterDescriptors | ICollection<IFilterDescriptor> | A collection of CompositeFilterDescriptor, except the ones that relate to the TreeListSearchBox. |
InsertedItem | TItem* | The data item that is being added in Inline or Popup edit mode. Not applicable for Incell editing. |
OriginalEditItem | TItem* | The original copy of the data item that is currently in edit mode. This TreeListState property holds the unmodified data item values. |
Page | int? | The current page index. Some user actions reset the page index to 1, such as filtering or changing the page size. |
ParentItem | TItem?* | The parent item of the current InsertedItem in Inline or Popup edit mode. The value is null is the new item is being added at root level. |
SearchFilter | IFilterDescriptor | The CompositeFilterDescriptor that holds the filter descriptors for the TreeListSearchBox. |
SelectedItems | ICollection<TItem> | The currently selected data item(s). |
Skip | int? | The number of scrolled data items when using virtual row scrolling. In other words, this is the number of rows above the currently visible ones. |
SortDescriptors | ICollection<SortDescriptor> | The currently applied sorts. |
TableWidth | string | The sum of all visible column widths. The initial value is always null regardless of the column configuration. The TableWidth value changes during column resizing together with ColumnStates and theOnStateChanged event does not fire separately for it. When you resize a column programmatically, and all other columns already have widths, you must update the TableWidth too, otherwise the other columns will resize unexpectedly. |
* TItem is the TreeList model type.
Events
The TreeList features two events, which are related to its state.
OnStateInit
The OnStateInit event fires when the TreeList is initializing. Use this event to:
- Define initial state, for example default initial sorting;
- Load and apply state that was previously saved in a database or in
localStorage.
The generic event argument is of type TreeListStateEventArgs<TItem> and has a TreeListState property. See Information in the TreeList State for details.
If you change the column order or number of columns in the TreeList declaration, this can break state restore. In such cases, either ignore the stored column state, or implement custom logic to restore only the columns that still exist in the TreeList.
To set the initial visibility of columns, better use the
Visibleparameter, rather than conditional markup for the whole column. TheVisibleparameter values will be present in the TreeList state and the columns collection count will remain the same. This makes it easier to reconcile changes.
The example below shows how to apply initial sorting, filtering and grouping.
Using TreeList OnStateInit
@using System.ComponentModel.DataAnnotations
@using Telerik.DataSource
@using Telerik.DataSource.Extensions
<TelerikTreeList Data="@TreeListData"
IdField="@nameof(Employee.Id)"
ParentIdField="@nameof(Employee.ParentId)"
FilterMode="@TreeListFilterMode.FilterMenu"
OnStateInit="@( (TreeListStateEventArgs<Employee> args) => OnTreeListStateInit(args) )"
Pageable="true"
Sortable="true"
Height="400px">
<TreeListColumns>
<TreeListColumn Field="@nameof(Employee.Name)" Expandable="true" />
<TreeListColumn Field="@nameof(Employee.Salary)" DisplayFormat="{0:C2}" Width="130px" />
<TreeListColumn Field="@nameof(Employee.HireDate)" DisplayFormat="{0:d}" Width="140px" />
<TreeListColumn Field="@nameof(Employee.IsDriver)" Width="120px" />
</TreeListColumns>
</TelerikTreeList>
@code {
private IEnumerable<Employee>? TreeListData { get; set; }
private EmployeeService TreeListEmployeeService { get; set; } = new();
private void OnTreeListStateInit(TreeListStateEventArgs<Employee> args)
{
// Sort sibling items by Salary
args.TreeListState.SortDescriptors.Add(new SortDescriptor()
{
Member = nameof(Employee.Salary),
SortDirection = ListSortDirection.Descending
});
// Filter by IsDriver
var driverColumnFilter = new CompositeFilterDescriptor()
{
FilterDescriptors = new FilterDescriptorCollection() {
new FilterDescriptor()
{
Member = nameof(Employee.IsDriver),
MemberType = typeof(bool),
Operator = FilterOperator.IsEqualTo,
Value = true
}
}
};
args.TreeListState.FilterDescriptors.Add(driverColumnFilter);
}
protected override async Task OnInitializedAsync()
{
TreeListData = await TreeListEmployeeService.Read();
}
public class Employee
{
public int Id { get; set; }
public int? ParentId { get; set; }
public bool HasChildren { get; set; }
[Required]
public string Name { get; set; } = string.Empty;
public string Notes { get; set; } = string.Empty;
[Required]
public decimal? Salary { get; set; }
[Required]
public DateTime? HireDate { get; set; }
public bool IsDriver { get; set; }
public override bool Equals(object? obj)
{
return obj is Employee && ((Employee)obj).Id == Id;
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
#region Data Service
public class EmployeeService
{
private List<Employee> Items { get; set; } = new();
private readonly int TreeLevelCount;
private readonly int RootItemCount;
private readonly int ChildItemCount;
private int LastId { get; set; }
private Random Rnd { get; set; } = Random.Shared;
public async Task<int> Create(Employee createdEmployee, Employee? parentEmployee)
{
await SimulateAsyncOperation();
createdEmployee.Id = ++LastId;
createdEmployee.ParentId = parentEmployee?.Id;
Items.Insert(0, createdEmployee);
if (parentEmployee != null)
{
parentEmployee.HasChildren = true;
}
return LastId;
}
public async Task<bool> Delete(Employee deletedEmployee)
{
await SimulateAsyncOperation();
if (Items.Contains(deletedEmployee))
{
DeleteChildren(deletedEmployee.Id);
Items.Remove(deletedEmployee);
if (deletedEmployee.ParentId.HasValue && !Items.Any(x => x.ParentId == deletedEmployee.ParentId.Value))
{
Items.First(x => x.Id == deletedEmployee.ParentId.Value).HasChildren = false;
}
return true;
}
return false;
}
public async Task<List<Employee>> Read()
{
await SimulateAsyncOperation();
return Items;
}
public async Task<DataSourceResult> Read(DataSourceRequest request)
{
return await Items.ToDataSourceResultAsync(request);
}
public async Task<bool> Update(Employee updatedEmployee)
{
await SimulateAsyncOperation();
int originalItemIndex = Items.FindIndex(x => x.Id == updatedEmployee.Id);
if (originalItemIndex != -1)
{
Items[originalItemIndex] = updatedEmployee;
return true;
}
return false;
}
private async Task SimulateAsyncOperation()
{
await Task.Delay(100);
}
private void DeleteChildren(int parentId)
{
List<Employee> children = Items.Where(x => x.ParentId == parentId).ToList();
foreach (Employee child in children)
{
DeleteChildren(child.Id);
}
Items.RemoveAll(x => x.ParentId == parentId);
}
private void PopulateChildren(List<Employee> items, int? parentId, int level)
{
int itemCount = level == 1 ? RootItemCount : ChildItemCount;
for (int i = 1; i <= itemCount; i++)
{
int itemId = ++LastId;
items.Add(new Employee()
{
Id = itemId,
ParentId = parentId,
HasChildren = level < TreeLevelCount,
Name = $"Employee Name {itemId}", // {level}-{i}
Notes = $"Multi-line\nnotes {itemId}",
Salary = Rnd.Next(1_000, 10_000) * 1.23m,
HireDate = DateTime.Today.AddDays(-Rnd.Next(365, 3650)),
IsDriver = itemId % 2 == 0
});
if (level < TreeLevelCount)
{
PopulateChildren(items, itemId, level + 1);
}
}
}
public EmployeeService(int treeLevelCount = 3, int rootItemCount = 3, int childItemCount = 2)
{
TreeLevelCount = treeLevelCount;
RootItemCount = rootItemCount;
ChildItemCount = childItemCount;
List<Employee> items = new();
PopulateChildren(items, null, 1);
Items = items;
}
}
#endregion Data Service
}
OnStateChanged
OnStateChanged fires when the user performs an action that changes the value of a property in the TreeList state. The event argument is of type TreeListStateEventArgs<TItem> and exposes these properties:
| Property | Type | Description |
|---|---|---|
PropertyName | string | Information about what changed in the TreeList state. The possible values match the property names of the TreeListState object. The possible values for the PropertyName are SortDescriptors, FilterDescriptors, SearchFilter, Page, Skip, ColumnStates, ExpandedItems, InsertedItem, OriginalEditItem, EditItem. |
TreeListState | TreeListState<TItem> | The current (up-to-date) TreeList state object. |
Here is some additional information about certain PropertyName values:
EditItemis used when the user starts editing an existing item.InsertedItemsignifies the user adding a new item in inline or popup edit mode. It's not applicable forIncellediting.OriginalEditItemis used when the user exits edit or insert mode via save or cancel.ColumnStatesis used for several column actions such as hiding, showing, locking, reordering and resizing.
Some user actions will trigger two
OnStateChangedevents with a differentPropertyNameeach time. These include filtering and searching. For example, filtering resets the current page to 1. First, the event will fire withPropertyNameequal to"FilterDescriptors", and thenPropertyNamewill be"Page". However, theTreeListStateproperty of the event argument will provide correct information about the overall TreeList state in both event handler executions.
We recommend using an
async Taskhandler for theOnStateChangedevent, in order to reduce re-rendering and avoid blocking UI updates if the handler will wait for a service to save the TreeList state somewhere.
To observe the changes in the TreeList state more easily, copy and run the following example in a local app and at full screen.
Find out how to get the applied filtering and sorting criteria.
Using TreeList OnStateChanged
@using System.ComponentModel.DataAnnotations
@using System.Text.Json
@using Telerik.DataSource
@using Telerik.DataSource.Extensions
<div id="demo-container">
<TelerikTreeList Data="@TreeListData"
IdField="@nameof(Employee.Id)"
ParentIdField="@nameof(Employee.ParentId)"
ConfirmDelete="true"
EditMode="@TreeListEditMode.Inline"
FilterMode="@TreeListFilterMode.FilterMenu"
OnCreate="@OnTreeListCreate"
OnUpdate="@OnTreeListUpdate"
OnStateChanged="@( (TreeListStateEventArgs<Employee> args) => OnTreeListStateChanged(args) )"
Pageable="true"
@bind-PageSize="@TreeListPageSize"
Reorderable="true"
Resizable="true"
@bind-SelectedItems="@TreeListSelectedItems"
SelectionMode="@TreeListSelectionMode.Multiple"
ShowColumnMenu="true"
Sortable="true"
Height="400px">
<TreeListSettings>
<TreeListPagerSettings PageSizes="@( new List<int?>() { null, 5, 10 } )" />
</TreeListSettings>
<TreeListToolBarTemplate>
<TreeListCommandButton Command="Add">Add Item</TreeListCommandButton>
<TreeListSearchBox />
</TreeListToolBarTemplate>
<TreeListColumns>
<TreeListCheckboxColumn SelectAll="true" />
<TreeListColumn Field="@nameof(Employee.Name)" Expandable="true" />
<TreeListColumn Field="@nameof(Employee.Salary)" DisplayFormat="{0:C2}" Width="130px" />
<TreeListColumn Field="@nameof(Employee.HireDate)" DisplayFormat="{0:d}" Width="140px" />
<TreeListColumn Field="@nameof(Employee.IsDriver)" Width="120px" />
<TreeListCommandColumn Width="160px">
<TreeListCommandButton Command="Add">Add</TreeListCommandButton>
<TreeListCommandButton Command="Edit">Edit</TreeListCommandButton>
<TreeListCommandButton Command="Save" ShowInEdit="true">Save</TreeListCommandButton>
<TreeListCommandButton Command="Cancel" ShowInEdit="true">Cancel</TreeListCommandButton>
</TreeListCommandColumn>
</TreeListColumns>
</TelerikTreeList>
<div id="console">
<code class="@TreeListStateChangedPropertyClass">OnStateChanged</code> count:
@OnStateChangedCount
<TelerikButton OnClick="@( () => OnStateChangedCount = 0 )">Reset</TelerikButton>
<br /><br />
Last <code>OnStateChanged</code> event:
<br />
<strong class="@TreeListStateChangedPropertyClass">PropertyName</strong>:
<code>"@TreeListStateChangedProperty"</code>
<br />
<strong>TreeListState</strong>:
<pre>
@( new MarkupString(TreeListStateString) )
</pre>
</div>
</div>
<style>
.first-of-two {
color: #f00;
}
.latest-changed-property {
color: #00f;
}
@@media (min-width: 800px) {
#demo-container {
display: flex;
align-items: flex-start;
gap: 1em;
}
#demo-container > .k-treelist {
flex: 2 2 800px;
}
#console {
height: 90vh;
overflow: auto;
flex: 1 0 300px;
border: 1px solid rgba(128, 128, 128, .3);
padding: 1em;
}
}
</style>
@code {
private IEnumerable<Employee>? TreeListData { get; set; }
private int TreeListPageSize { get; set; } = 5;
private IEnumerable<Employee> TreeListSelectedItems { get; set; } = new List<Employee>();
private EmployeeService TreeListEmployeeService { get; set; } = new();
private int OnStateChangedCount { get; set; }
private string TreeListStateChangedProperty { get; set; } = string.Empty;
private string TreeListStateChangedPropertyClass { get; set; } = string.Empty;
private string TreeListStateString { get; set; } = string.Empty;
private bool _doubleStateChanged { get; set; }
private List<string> _operationsWithMultipleStateChanged = new List<string>() {
"FilterDescriptors",
"SearchFilter"
};
private async Task OnTreeListStateChanged(TreeListStateEventArgs<Employee> args)
{
if (_doubleStateChanged)
{
_doubleStateChanged = false;
await Task.Delay(1500);
TreeListStateChangedPropertyClass = string.Empty;
}
++OnStateChangedCount;
TreeListStateChangedProperty = args.PropertyName;
// serialize the TreeListState and highlight the changed property
TreeListStateString = JsonSerializer.Serialize(args.TreeListState, new JsonSerializerOptions() { WriteIndented = true })
.Replace($"\"{TreeListStateChangedProperty}\"", $"\"<strong class='latest-changed-property'>{TreeListStateChangedProperty}</strong>\"");
// highlight first TreeListStateChangedProperty during filtering, grouping and search
if (_operationsWithMultipleStateChanged.Contains(TreeListStateChangedProperty))
{
_doubleStateChanged = true;
TreeListStateChangedPropertyClass = "first-of-two";
}
}
private async Task OnTreeListCreate(TreeListCommandEventArgs args)
{
var createdItem = (Employee)args.Item;
var parentItem = (Employee?)args.ParentItem;
await TreeListEmployeeService.Create(createdItem, parentItem);
TreeListData = await TreeListEmployeeService.Read();
}
private async Task OnTreeListUpdate(TreeListCommandEventArgs args)
{
var updatedItem = (Employee)args.Item;
await TreeListEmployeeService.Update(updatedItem);
TreeListData = await TreeListEmployeeService.Read();
}
protected override async Task OnInitializedAsync()
{
TreeListData = await TreeListEmployeeService.Read();
}
public class Employee
{
public int Id { get; set; }
public int? ParentId { get; set; }
public bool HasChildren { get; set; }
[Required]
public string Name { get; set; } = string.Empty;
public string Notes { get; set; } = string.Empty;
[Required]
public decimal? Salary { get; set; }
[Required]
public DateTime? HireDate { get; set; }
public bool IsDriver { get; set; }
public override bool Equals(object? obj)
{
return obj is Employee && ((Employee)obj).Id == Id;
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
#region Data Service
public class EmployeeService
{
private List<Employee> Items { get; set; } = new();
private readonly int TreeLevelCount;
private readonly int RootItemCount;
private readonly int ChildItemCount;
private int LastId { get; set; }
private Random Rnd { get; set; } = Random.Shared;
public async Task<int> Create(Employee createdEmployee, Employee? parentEmployee)
{
await SimulateAsyncOperation();
createdEmployee.Id = ++LastId;
createdEmployee.ParentId = parentEmployee?.Id;
Items.Insert(0, createdEmployee);
if (parentEmployee != null)
{
parentEmployee.HasChildren = true;
}
return LastId;
}
public async Task<bool> Delete(Employee deletedEmployee)
{
await SimulateAsyncOperation();
if (Items.Contains(deletedEmployee))
{
DeleteChildren(deletedEmployee.Id);
Items.Remove(deletedEmployee);
if (deletedEmployee.ParentId.HasValue && !Items.Any(x => x.ParentId == deletedEmployee.ParentId.Value))
{
Items.First(x => x.Id == deletedEmployee.ParentId.Value).HasChildren = false;
}
return true;
}
return false;
}
public async Task<List<Employee>> Read()
{
await SimulateAsyncOperation();
return Items;
}
public async Task<DataSourceResult> Read(DataSourceRequest request)
{
return await Items.ToDataSourceResultAsync(request);
}
public async Task<bool> Update(Employee updatedEmployee)
{
await SimulateAsyncOperation();
int originalItemIndex = Items.FindIndex(x => x.Id == updatedEmployee.Id);
if (originalItemIndex != -1)
{
Items[originalItemIndex] = updatedEmployee;
return true;
}
return false;
}
private async Task SimulateAsyncOperation()
{
await Task.Delay(100);
}
private void DeleteChildren(int parentId)
{
List<Employee> children = Items.Where(x => x.ParentId == parentId).ToList();
foreach (Employee child in children)
{
DeleteChildren(child.Id);
}
Items.RemoveAll(x => x.ParentId == parentId);
}
private void PopulateChildren(List<Employee> items, int? parentId, int level)
{
int itemCount = level == 1 ? RootItemCount : ChildItemCount;
for (int i = 1; i <= itemCount; i++)
{
int itemId = ++LastId;
items.Add(new Employee()
{
Id = itemId,
ParentId = parentId,
HasChildren = level < TreeLevelCount,
Name = $"Employee Name {itemId}", // {level}-{i}
Notes = $"Multi-line\nnotes {itemId}",
Salary = Rnd.Next(1_000, 10_000) * 1.23m,
HireDate = DateTime.Today.AddDays(-Rnd.Next(365, 3650)),
IsDriver = itemId % 2 == 0
});
if (level < TreeLevelCount)
{
PopulateChildren(items, itemId, level + 1);
}
}
}
public EmployeeService(int treeLevelCount = 3, int rootItemCount = 3, int childItemCount = 2)
{
TreeLevelCount = treeLevelCount;
RootItemCount = rootItemCount;
ChildItemCount = childItemCount;
List<Employee> items = new();
PopulateChildren(items, null, 1);
Items = items;
}
}
#endregion Data Service
}
Methods
The GetState and SetStateAsync methods of the TreeList instance let you get and set the current TreeList state on demand at any time after OnStateInit.
-
GetStatereturns the current TreeList state, so you can save it or retrieve specific information. For example, you can useGetStateto get the current filters, sorts, and page number. Or, you can get the current TreeList column properties like order index, width, and others). -
SetStateAsyncreceives an instance of aTreeListState<TItem>object and applies it to the TreeList. For example, you can have a button that puts the TreeList in a certain configuration programmatically, for example sort or filter the data, enter or exit edit mode, expand or collapse rows, etc.
If you want to make changes to the current TreeList state:
- First, get the current state with the
GetStatemethod. - Apply the desired modifications to the obtained
TreeListStateobject. - Set the modified state object via the
SetStateAsyncmethod.
Do not use
GetState()in theOnStateInitorOnStateChangedevents. Do not useSetStateAsync()inOnStateInit. Instead, get or set theTreeListStateproperty of the event argument.Avoid calling
SetStateAsyncin the TreeList CRUD methods (such asOnUpdate,OnEdit,OnCreate,OnCancel). Doing so may lead to unexpected results because the TreeList has more logic to execute after these events.
To reset the TreeList state to its initial markup configuration, call
SetStateAsync(null).To reset the TreeList state to a completely new configuration, create a
new TreeListState<T>()and apply the settings there. Then pass the state object toSetStateAsync().
SetStateAsync Examples
The tabs below show how to set the TreeList state and control filtering, sorting and other TreeList features.
If you want to set an initial state to the TreeList, use a similar snippet, but in the
OnStateInit event
@* This snippet shows how to set sorting state to the TreeList from your code *@
@using Telerik.DataSource;
<TelerikButton OnClick="@SetTreeListSort">Set sorted state</TelerikButton>
<TelerikTreeList Data="@Data"
ItemsField="@(nameof(Employee.DirectReports))"
Reorderable="true"
Resizable="true"
Sortable="true"
Pageable="true"
Width="850px"
@ref="TreeListRef">
<TreeListColumns>
<TreeListColumn Field="Name" Expandable="true" Width="320px" />
<TreeListColumn Field="Id" Editable="false" Width="120px" />
<TreeListColumn Field="EmailAddress" Width="220px" />
<TreeListColumn Field="HireDate" Width="220px" />
</TreeListColumns>
</TelerikTreeList>
@code {
public TelerikTreeList<Employee> TreeListRef { get; set; } = new TelerikTreeList<Employee>();
async Task SetTreeListSort()
{
var sortedState = new TreeListState<Employee>()
{
SortDescriptors = new List<SortDescriptor>()
{
new SortDescriptor(){ Member = nameof(Employee.Id), SortDirection = ListSortDirection.Descending }
}
};
await TreeListRef.SetStateAsync(sortedState);
}
public List<Employee> Data { get; set; }
// sample model
public class Employee
{
// hierarchical data collections
public List<Employee> 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<List<Employee>> GetTreeListData()
{
List<Employee> data = new List<Employee>();
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<Employee>(), // 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<Employee>(), // 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);
}
}If you want to alter the filters for a specific column, do not use more than one
FilterDescriptorinFilterRowmode, and more than twoFilterDescriptorsinFilterMenumode. Otherwise additional descriptors will not show up in the UI. This means that you may need to replace or modify an existing descriptor, rather than add a new one.Inactive filter descriptors in
FilterMenumode are distinguished by theirnullValue.
Equals Comparison
State properties that pertain to data items (for example, edited item or selected items) are 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 (object) is a reference check and the reference from the restored state is very unlikely to match the current reference in the TreeList data. Thus, you may want to override the .Equals() method of the TreeList model class, so that it compares by ID, or otherwise re-populate the models in the state object with the new model references from the TreeList data.
Examples
You can find multiple examples for using the TreeList state in the following Knowledge Base articles:
- Save and load the TreeList state from
localStorage - Save the TreeList state in a WebAssembly app
- Override a user action that changes the TreeList state, for example, sort descending first
- Initiate programmatic editing or inserting of a TreeList row
- Get current TreeList column state (order index, width, and others)