New to Telerik UI for Blazor? Download free 30-day trial

TreeList InCell Editing

In Cell editing allows the user to click the cell and type the new value. When they remove focus from the treelist or current row, the OnUpdate event fires, where the data-access logic can move it to the actual data source.

You can also use the Tab, Shift+Tab and Enter keys to move between edited cells quickly to perform fast data updates. This lets the user edit efficiently, with few actions, like in Excel, while avoiding delays and re-renders from data updates that will break up that flow. Command columns and non-editable columns are not part of this keyboard navigation.

When validation is not satisfied, you cannot close the cell (exit its edit mode), but you can cancel changes by pressing Esc.

Sections in this article:

Basics

To enable InCell editing mode, set the EditMode property of the treelist to Telerik.Blazor.TreeListEditMode.Incell. You can handle the OnUpdate, OnCreate and OnDelete events to perform the CUD operations, as shown in the example below.

To add a new item, you must also add a toolbar with an Add command. OnCreate will fire immediately when you click the Add button, see the Notes below.

The OnUpdate event always fires for the last edited cell on the row - when you remove focus from the treelist, or when you press Enter to go to the next row.

Reduced need for command buttons and user actions. The treelist events let you handle data operations in InCell edit mode (see the code comments for details)

@using System.ComponentModel.DataAnnotations @* for the validation attributes *@

Click a cell, edit it and click outside of the treelist to see the change. You can also use Tab, Shift+Tab and Enter to navigate between the cells.
<br />
Editing is cancelled for the first record.
<br />

<TelerikTreeList Data="@Data"
                 EditMode="@TreeListEditMode.Incell"
                 OnUpdate="@UpdateItem"
                 OnDelete="@DeleteItem"
                 OnCreate="@CreateItem"
                 OnEdit="@OnEditHandler"
                 OnCancel="@OnCancelHandler"
                 Pageable="true" ItemsField="@(nameof(Employee.DirectReports))"
                 Width="850px">
    <TreeListToolBar>
        <TreeListCommandButton Command="Add" Icon="add">Add</TreeListCommandButton>
    </TreeListToolBar>
    <TreeListColumns>
        <TreeListCommandColumn Width="200px">
            <TreeListCommandButton Command="Add" Icon="plus">Add Child</TreeListCommandButton>
            <TreeListCommandButton Command="Delete" Icon="delete">Delete</TreeListCommandButton>
        </TreeListCommandColumn>

        <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 List<Employee> Data { get; set; }

    // Sample CUD operations for the local data
    async Task UpdateItem(TreeListCommandEventArgs args)
    {
        var item = args.Item as Employee;

        // perform actual data source operations here through your service
        await MyService.Update(item);

        // update the local view-model data with the service data
        await GetTreeListData();
    }

    async Task CreateItem(TreeListCommandEventArgs args)
    {
        var item = args.Item as Employee;
        var parentItem = args.ParentItem as Employee;

        // perform actual data source operations here through your service
        await MyService.Create(item, parentItem);

        // update the local view-model data with the service data
        await GetTreeListData();
    }

    async Task DeleteItem(TreeListCommandEventArgs args)
    {
        var item = args.Item as Employee;

        // perform actual data source operations here through your service
        await MyService.Delete(item);

        // update the local view-model data with the service data
        await GetTreeListData();
    }

    // OnEdit handler

    async Task OnEditHandler(TreeListCommandEventArgs args)
    {
        Employee empl = args.Item as Employee;
        if (empl.Id == 1)
        {
            // prevent opening for edit based on condition
            args.IsCancelled = true;
            Console.WriteLine("You cannot edit this item");
        }
    }

    // OnCancel handler

    async Task OnCancelHandler(TreeListCommandEventArgs args)
    {
        Employee empl = args.Item as Employee;
        // if necessary, perform actual data source operation here through your service
    }


    // sample model

    public class Employee
    {
        public int Id { get; set; }
        [Required]
        public string Name { get; set; }
        public string EmailAddress { get; set; }
        public DateTime HireDate { get; set; }

        public List<Employee> DirectReports { get; set; }
        public bool HasChildren { get; set; }

        // Used for the editing so replacing the object in the view-model data
        // will treat it as the same object and keep its state - otherwise it will
        // collapse after editing is done, which is not what the user would expect
        public override bool Equals(object obj)
        {
            if (obj is Employee)
            {
                return this.Id == (obj as Employee).Id;
            }
            return false;
        }
    }

    // data generation

    async Task GetTreeListData()
    {
        Data = await MyService.Read();
    }

    protected override async Task OnInitializedAsync()
    {
        await GetTreeListData();
    }

    // the following static class mimics an actual data service that handles the actual data source
    // replace it with your actual service through the DI, this only mimics how the API can look like and works for this standalone page
    public static class MyService
    {
        private static List<Employee> _data { get; set; } = new List<Employee>();
        // used in this example for data generation and retrieval for CUD operations on the current view-model data
        private static int LastId { get; set; } = 1;

        public static async Task Create(Employee itemToInsert, Employee parentItem)
        {
            InsertItemRecursive(_data, itemToInsert, parentItem);
        }

        public static async Task<List<Employee>> Read()
        {
            if (_data.Count < 1)
            {
                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>(),
                        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<Employee>(),
                            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);
        }

        public static async Task Update(Employee itemToUpdate)
        {
            UpdateItemRecursive(_data, itemToUpdate);
        }

        public static async Task Delete(Employee itemToDelete)
        {
            RemoveChildRecursive(_data, itemToDelete);
        }

        // sample helper methods for handling the view-model data hierarchy
        static void UpdateItemRecursive(List<Employee> items, Employee itemToUpdate)
        {
            for (int i = 0; i < items.Count; i++)
            {
                if (items[i].Id.Equals(itemToUpdate.Id))
                {
                    items[i] = itemToUpdate;
                    return;
                }

                if (items[i].DirectReports?.Count > 0)
                {
                    UpdateItemRecursive(items[i].DirectReports, itemToUpdate);
                }
            }
        }

        static void RemoveChildRecursive(List<Employee> 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;
                    }
                }
            }
        }

        static void InsertItemRecursive(List<Employee> Data, Employee insertedItem, Employee parentItem)
        {
            insertedItem.Id = LastId++;
            if (parentItem != null)
            {
                parentItem.HasChildren = true;
                if (parentItem.DirectReports == null)
                {
                    parentItem.DirectReports = new List<Employee>();
                }

                parentItem.DirectReports.Insert(0, insertedItem);
            }
            else
            {
                Data.Insert(0, insertedItem);
            }
        }
    }
}

The result from the code snippet above, after the user clicks in the Name column of the row with ID 4

Notes

The InCell edit mode provides a specific user experience that aims at fast efficient data entry. This requires that it behaves a differently than other edit modes. Please review the notes below to get a better understanding of these specifics:

General

  • When the InCell Edit Mode is enabled and you want to enable item selection a <TreeListCheckboxColumn /> must be added to the <Columns> collection. More information on that can be read in the Selection article.

  • It is up to the data access logic to save the data once it is changed in the data collection. The example above showcases when that happens and adds some code to provide a visual indication of the change. In a real application, the code for handling data updates may be entirely different.

  • If validation is not satisfied, you cannot open another cell for editing, and you need to either satisfy the validation, or press Esc to revert its value to the original one that should, ideally, satisfy validation.

  • If you click the "Add" button on a row that is not expanded you will not see it in the UI opened for editing. There will be an OnCreate call to insert a record as its child, but editing (and inserting) items is a separate operation from expanding items and the treelist should not invoke these changes arbitrarily. There can be other handlers, business logic or load-on-demand attached to that action, and that changes the users state. This also applies to items that currently have no child items - they will now have a child item, but it will not expand and open for editing.

Events Sequence

  • The OnCreate event will fire as soon as you click the Add button so you can add the new row to the treelist Data - this will let it show up in the treelist, and then enter edit mode for the first editable column (to fire OnEdit and let the user alter the column). This means you should have default values that satisfy any initial validation and requirements your models may have.

    • This means that there is no actual inserted item, an item in InCell editing is always in Edit mode, never in Insert mode. Thus, you cannot use the InsertedItem field of the treelist State. If you want to insert items programmatically in the treelist, alter the Data collection, and use the OriginalEditItem feature of the state (see the Initiate Editing or Inserting of an Item example - it can put the InLine and PopUp edit modes in Insert mode, but this cannot work for InCell editing).
  • The OnEdit event fires once per row - when the first cell from a row is opened for editing. Moving with the keyboard (Tab or Shift+Tab) between its cells does not fire events so that the treelist does not re-render, and there is no lag for the user, especially from slow data operations such as OnUpdate. This caters to the user experience so they can input data quickly and efficiently.

  • If you use the keyboard to navigate between open cells, OnUpdate will fire only when the entire row loses focus, not for each cell, so you will not need additional actions to open a new cell.

  • If there is a cell that is being edited at the moment, clicking on a cell will first close the current cell and fire OnUpdate. To start editing the new cell in such a case you will need a second click.

  • The OnCancel event can work only with the keyboard (when you press Esc). The Cancel command button is not supported. Clicking outside the currently edited cell will trigger the OnUpdate event and thus, clicking on the Cancel command button will not fire the OnCancel event because an update has already occured.

Editor Template

When using an editor template, the grid cannot know what the custom editor needs to do, what it contains, and when it needs to close the cell and update the data, because this is up to the editor. This has the following implications:

  • The treelist will still capture Enter and Tab keypresses when the cell is focused, and will close the cell with the corresponding OnUpdate call. You can either use that (e.g., a standard input will let the keypress event propagate to the treelist cell), or you can prevent the event propagation and use only your business logic. If you don't do anything, you will get the default treelist behavior for the keyboard navigation even with custom editors.

  • The treelist can no longer capture the onblur event in a custom editor like it does for built-in editors. It uses it to call OnUpdate when the user clicks away from the current row with the mouse. So, when an editor template is open, clicking away will not close it and save the row.

    If you want to get this behavior, you can use the treelist state to close the cell and you can also invoke the desired operations on the data according to your business logic. For example, a suitable event the Telerik input components provide is OnBlur.

    .razor

    <EditorTemplate>
        @{
            CurrentlyEditedLine = context as SampleData;
            <TelerikTextBox OnBlur="@CloseEditorAndSave" 
                Width="100%" @bind-Value="@CurrentlyEditedLine.LastName">
            </TelerikTextBox>
    
        }
    </EditorTemplate>
    

    C#

    SampleData CurrentlyEditedLine { get; set; }
    TelerikTreeList<SampleData> TreeList { get; set; }
    
    async Task CloseEditorAndSave()
    {
        var state = TreeList?.GetState();
        if (state.EditItem != null)
        {
            // we can reuse the code from the OnUpdate handler
            await UpdateHandler(new TreeListCommandEventArgs()
            {
                Item = state.EditItem
            });
    
            // use the state to remove the edited item (close the editor)
            state.EditItem = null;
            state.OriginalEditItem = null;
            await TreeList.SetState(state);
        }
    }
    
  • Using an editor template requires that there is a focusable element in the editor template in order to maintain the tab order when using the keyboard. For example, if you prevent editing based on a runtime condition (setting Editable=false for the entire column does not require this), you must provide a focusable element, here is one way to add such an element:

    .razor

    <EditorTemplate>
    @{
        if (myCurrentEditCondition)
        {
            <MyCustomEditor />
        }
        else
        {
            <div tabindex="0">editing not allowed</div>
        }
    }
    

See Also

In this article
Not finding the help you need? Improve this article