TreeList Inline Editing
Inline TreeList editing lets users modify all values on a TreeList row. The edit process starts and ends with clicking of command buttons on the respective row. Inline editing can be more intuitive for beginner users, compared to in-cell editing.
Make sure to read the TreeList CRUD Operations article first.
Basics
To use inline TreeList editing, set the TreeList EditMode
parameter to TreeListEditMode.Inline
. During inline editing, only one table row is in edit mode at a time. Users can:
- Press Tab or Shift + Tab to focus the next or previous editable cell.
- Click the Save command button or press Enter to confirm the current row changes and exit edit mode.
- Click the Cancel command button or press ESC to cancel the current row changes and exit edit mode.
- Peform another TreeList operation, for example, paging or sorting, to cancel the current edit operation.
Commands
Inline add, edit, and delete operations use the following command buttons:
- Add
- Delete
- Edit
- Save
- Cancel
Without using the above command buttons, the application can:
- Manage add or edit mode programmatically through the TreeList state.
- Modify data items directly in the TreeList data source. Rebind the TreeList afterwards.
In inline edit mode, the TreeList commands execute row by row and the corresponding TreeList events also fire row by row. This is similar to popup editing and unlike in-cell editing, where commands and events relate to cells.
When validation is not satisfied, clicking the Save, Delete or Add command buttons has no effect, but users can still navigate between all input components in the row to complete the editing.
Integration with Other Features
Here is how the component behaves when the user tries to use add and edit operations together with other component features. Also check the common information on this topic for all edit modes.
Add, Edit
This section explains what happens when the component is already in add or edit mode, and the user tries to add or edit another row.
- If the validation is not satisfied, the component blocks the user action until they complete or cancel the current add or edit operation.
- If the validation is satisfied, then editing aborts and the component fires
OnCancel
.
Delete, Filter, Group, Page, Search, Sort
If the component is already in add or edit mode, and the user tries to perform another data operation, then editing aborts and the component fires OnCancel
.
Example
The example below shows how to:
- Implement inline TreeList CRUD operations with the minimal required number of events.
- Bind an editable TreeList to flat data. Check the [popup editing example] for an implementation with hierarchical data.
- Use the
OnCreate
,OnDelete
andOnUpdate
events to make changes to the TreeList data source. - Query the data service and reload the TreeList
Data
when the create, delete, or update operation is complete. - Use
DataAnnotations
validation for some model class properties. - Define the
Id
column as non-editable. - Customize the
Notes
column editor without using anEditorTemplate
. - Confirm Delete commands with the built-in TreeList Dialog. You can also intercept item deletion with a separate Dialog or a custom popup.
- Override the
Equals()
method of the TreeList model class to prevent collapsing of updated items. - Toggle the
HasChildren
property value of parent items when they lose all their children or gain their first child item. - Delete all children of a deleted parent item.
TreeList inline editing
@using System.ComponentModel.DataAnnotations
@using Telerik.DataSource
@using Telerik.DataSource.Extensions
<TelerikTreeList Data="@TreeListData"
IdField="@nameof(Employee.Id)"
ParentIdField="@nameof(Employee.ParentId)"
ConfirmDelete="true"
EditMode="@TreeListEditMode.Inline"
OnCreate="@OnTreeListCreate"
OnDelete="@OnTreeListDelete"
OnUpdate="@OnTreeListUpdate"
Height="400px">
<TreeListToolBarTemplate>
<TreeListCommandButton Command="Add">Add Item</TreeListCommandButton>
</TreeListToolBarTemplate>
<TreeListColumns>
<TreeListColumn Field="@nameof(Employee.Id)" Editable="false" Width="60px" />
<TreeListColumn Field="@nameof(Employee.Name)" Expandable="true" />
<TreeListColumn Field="@nameof(Employee.Notes)" EditorType="@TreeListEditorType.TextArea" Width="120px">
<Template>
@{ var dataItem = (Employee)context; }
<div style="white-space:pre">@dataItem.Notes</div>
</Template>
</TreeListColumn>
<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="80px" />
<TreeListCommandColumn Width="200px">
<TreeListCommandButton Command="Add">Add</TreeListCommandButton>
<TreeListCommandButton Command="Edit">Edit</TreeListCommandButton>
<TreeListCommandButton Command="Save" ShowInEdit="true">Save</TreeListCommandButton>
<TreeListCommandButton Command="Cancel" ShowInEdit="true">Cancel</TreeListCommandButton>
<TreeListCommandButton Command="Delete">Delete</TreeListCommandButton>
</TreeListCommandColumn>
</TreeListColumns>
</TelerikTreeList>
@code {
private IEnumerable<Employee>? TreeListData { get; set; }
private EmployeeService TreeListEmployeeService { get; set; } = new();
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 OnTreeListDelete(TreeListCommandEventArgs args)
{
var deletedItem = (Employee)args.Item;
await TreeListEmployeeService.Delete(deletedItem);
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
}