New to Telerik UI for BlazorStart a free 30-day trial

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:

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 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 and OnUpdate 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 an EditorTemplate.
  • 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
}

See Also