TreeList InCell Editing
In Cell editing allows the user to click cells and type new values immediately like in Excel. There is no need for Edit, Update and Cancel buttons.
Users can use the Tab
, Shift+Tab
and Enter
keys to move between edited cells quickly. If validation is not satisfied, the user cannot exit edit mode, unless they satisfy validation, or cancel changes by pressing Esc
.
Command columns and non-editable columns are skipped while tabbing.
The InCell edit mode provides a specific user experience and behaves differently than other edit modes. Please review the notes below to get a better understanding of these specifics.
Sections in this article
- Basics
- Event Sequence
- Incell Editing and Selection
- Adding Children to Collapsed Items
- Editor Template
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 add a toolbar with an Add
command. OnCreate
will fire immediately when you click the Add
button - see Event Sequence below.
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.
@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">
<TreeListToolBarTemplate>
<TreeListCommandButton Command="Add" Icon="@SvgIcon.Plus">Add</TreeListCommandButton>
</TreeListToolBarTemplate>
<TreeListColumns>
<TreeListCommandColumn Width="200px">
<TreeListCommandButton Command="Add" Icon="@SvgIcon.Plus">Add Child</TreeListCommandButton>
<TreeListCommandButton Command="Delete" Icon="@SvgIcon.Trash">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);
}
}
}
}
Event Sequence
-
The
OnCreate
event will fire as soon as you click theAdd
button. The Grid will render the new row and enter edit mode for the first editable column (to fireOnEdit
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 theData
collection, and use theOriginalEditItem
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).
- 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
The
OnEdit
event fires every time a cell is opened for editing. Until version 2.27, the event fired once per row - when the user edits a cell from a different row.The
OnUpdate
event fires every time an edited cell is closed. Until version 2.27, the event fired once per row - when the currently edited row loses focus.If there is a cell that is being edited at the moment, clicking on another cell will first close the current cell and fire
OnUpdate
. To start editing the new cell, you need a second click. When the user removes focus from the TreeList or the current row, theOnUpdate
event fires, where the data-access logic can move it to the actual data source.The
OnCancel
event works only when pressingEsc
. TheCancel
command button is not supported. Clicking outside the currently edited cell will triggerOnUpdate
and thus, clicking on theCancel
command button will not fire theOnCancel
event, because an update has already occurred.
Incell Editing and Selection
- To enable item selection with InCell Edit Mode, add a
<TreeListCheckboxColumn />
to the<Columns>
collection. More information on that can be read in the Selection article.
Adding Children to Collapsed Items
If you click the "Add" button on a row that is not expanded, you will not see the new child row in the UI. There will be an OnCreate
call to insert a record, 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.
Editor Template
The incell editor template requires a focusable element to maintain the tab order when using the keyboard. If you prevent editing based on a runtime condition, you must provide some focusable element. (Setting Editable=false
for the entire column does not require a focusable element.) Here is one way to add a focusable non-editable element:
<EditorTemplate>
@{
if (myCurrentEditCondition)
{
<MyCustomEditor />
}
else
{
<div tabindex="0">editing not allowed</div>
}
}
</EditorTemplate>