TreeList In-Cell Editing
In-cell editing allows users to click TreeList data cells and type new values like in Excel. There is no need for command buttons to enter and exit edit mode. Users can quickly move between the editable cells and rows by using keyboard navigation.
The in-cell edit mode provides a different user experience, compared to the inline and popup edit modes. In-cell edit mode can be more convenient for advanced users, fast users, or users who prefer keyboard navigation rather than clicking command buttons.
Make sure to read the TreeList CRUD Operations article first.
Basics
To use in-cell TreeList editing, set the TreeList EditMode
parameter to TreeListEditMode.Incell
. During in-cell editing, only one table cell is in edit mode at a time. Users can:
- Press Tab or Shift + Tab to confirm the current value and edit the next or previous cell.
- Press Enter to confirm the current value and edit the cell below.
- Press ESC to cancel the current change and exit edit mode.
- Click on another cell to confirm the current value and edit the new cell.
- Click outside the TreeList to confirm the current value and exit edit mode.
- Peform another TreeList operation, for example, paging or sorting, to cancel the current edit operation.
Command columns and non-editable columns are skipped while tabbing.
Commands
In-cell add, edit, and delete operations use the following command buttons:
- Add
- Delete
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.
Unlike inline editing, the in-cell edit mode does not use Edit, Save, and Cancel command buttons.
Events
Users enter and exit in-cell edit mode cell by cell, so the OnEdit
, OnCancel
, and OnUpdate
events also fire cell by cell.
In in-cell edit mode, the OnAdd
and OnCreate
events fire immediately one after the other, unless OnAdd
is cancelled. This means that:
- The new row is added to the TreeList data source before users start editing it.
- Valid default values are recommended.
- Users are always editing existing rows, not adding new ones.
- The
InsertedItem
property of the TreeList state is not used and is alwaysnull
. - To add a new row programmatically and put it in edit mode, use the
OriginalEditItem
,EditItem
, andEditField
properties of the TreeList state.
The above algorithm is different from inline and popup editing where new rows are only added to the data source after users populate them with valid values.
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 cell.
- 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 completes and the component fires
OnUpdate
.
Delete, Filter, Page, Search, Sort
This section explains what happens when the user tries to perform another data operation, while the component is already in add or edit mode.
- If the validation is satisfied, then editing completes and the component fires
OnUpdate
. - If the validation is not satisfied, then editing aborts and the component fires
OnCancel
.
Selection
To enable row selection with in-cell edit mode, use a checkbox column. More information on that can be read in the Row Selection article.
To see how to select the row that is currently in in-cell edit mode without using a <TreeListCheckboxColumn />
, see the Row Selection in Edit with InCell EditMode Knowledge Base article.
Cell selection is not supported with in-cell edit mode.
Example
The example below shows how to:
- Implement in-cell 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.
Basic TreeList in-cell editing configuration
@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.Incell"
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="120px">
<TreeListCommandButton Command="Add">Add</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
}