Blazor TreeList CRUD Operations Overview
CRUD operations with the TreeList for Blazor support validation and are done through the dedicated CRUD events it exposes for data editing. You can use them to transfer the changes to the underlying data source (for example, call a service that will actually work with the database, and not only with the view data).
To enable TreeList editing, set the EditMode
attribute to the desired edit mode.
Sections in this article:
Basics
The TreeList offers several editing modes with different user experience through the EditMode
property that is a member of the TreeListEditMode
enum:
-
None
- the defaultTreeListEditMode
value. The built-inAdd
andEdit
commands don't work in this mode. -
Incell
- editing is done in the current cell with a double click -
Inline
- editing is done for the entire row with an Edit Command Button -
Popup
- editing is done in a popup for the entire row with an Edit Command Button, and model Validation through Data Annotation attributes is available.
Events
Here are the available events and command buttons that you need to use for editing records in a TreeList.
-
OnAdd
- fires when theAdd
command button for a newly added item is clicked. Cancellable. -
OnCreate
- fires when theSave
command button for a newly added item is clicked. Cancellable. -
OnUpdate
- fires when theSave
command button is clicked on an existing item. Cancellable. The model reference is a copy of the original data source item. -
OnDelete
- fires when theDelete
command button is clicked. The event is cancellable, and you can also display a delete confirmation dialog before the deletion. -
OnEdit
- fires when the user is about to enter edit mode for an existing row. Cancellable. -
OnCancel
- fires when the user clicks theCancel
command button. Allows you to undo the changes to the data in the view data. Cancellable.
The CUD event handlers receive an argument of type TreeListCommandEventArgs
that exposes the following fields:
-
IsCancelled
- a boolean field indicating whether the treelist operation is to be prevented. You can use it to prevent the user action from affecting the treelist (for example, prevent a row from opening for edit, or the user from cancelling an edit operation). -
IsNew
- a boolean field indicating whether the item was just added through the treelist. Lets you differentiate a data source Create operation from Update operation in theOnClick
event of a command button. -
Item
- an object you can cast to your model class to obtain the current data item. -
ParentItem
- an object you can cast to your model class to obtain the parent of current data item. Will benull
if the current item is at the root level. -
Field
- specific to InCell editing - indicates which is the model field the user changed when updating data. -
Value
- specific to InCell editing - indicates what is the new value the user changed when updating data.
You can initiate editing or inserting of an item from anywhere on the page (buttons outside of the treelist, or components in a column template) through the treelist state.
Customize The Editor Fields
You can customize the editors rendered in the TreeList by providing the EditorType
attribute, exposed on the <TreeListColumn>
, or by using the Editor Template. The EditorType
attribute accepts a member of the TreeListEditorType
enum:
Field data type | TreeListEditorType enum members |
---|---|
Text |
TreeListEditorType.TextArea TreeListEditorType.TextBox
|
Boolean |
TreeListEditorType.CheckBox TreeListEditorType.Switch
|
DateTime |
TreeListEditorType.DatePicker TreeListEditorType.DateTimePicker TreeListEditorType.TimePicker
|
@* The usage of the EditorType parameter *@
@using System.ComponentModel.DataAnnotations
<TelerikTreeList Data="@Data"
EditMode="@TreeListEditMode.Inline"
OnUpdate="@UpdateItem"
OnDelete="@DeleteItem"
OnCreate="@CreateItem"
OnCancel="@OnCancelHandler"
Pageable="true" ItemsField="@(nameof(Employee.DirectReports))"
Width="850px">
<TreeListToolBarTemplate>
<TreeListCommandButton Command="Add" Icon="@SvgIcon.Plus">Add</TreeListCommandButton>
</TreeListToolBarTemplate>
<TreeListColumns>
<TreeListCommandColumn Width="280px">
<TreeListCommandButton Command="Add" Icon="@SvgIcon.Plus">Add Child</TreeListCommandButton>
<TreeListCommandButton Command="Edit" Icon="@SvgIcon.Pencil">Edit</TreeListCommandButton>
<TreeListCommandButton Command="Delete" Icon="@SvgIcon.Trash">Delete</TreeListCommandButton>
<TreeListCommandButton Command="Save" Icon="@SvgIcon.Save" ShowInEdit="true">Update</TreeListCommandButton>
<TreeListCommandButton Command="Cancel" Icon="@SvgIcon.Cancel" ShowInEdit="true">Cancel</TreeListCommandButton>
</TreeListCommandColumn>
<TreeListColumn Field="Name" Expandable="true" Width="320px" />
<TreeListColumn Field="Id" Editable="false" Width="120px" />
<TreeListColumn Field="EmailAddress" Width="220px" />
<TreeListColumn Field="HireDate"
EditorType="@TreeListEditorType.DateTimePicker"
Width="220px" />
</TreeListColumns>
</TelerikTreeList>
@code {
public List<Employee> Data { get; set; }
async Task UpdateItem(TreeListCommandEventArgs args)
{
var item = args.Item as Employee;
await MyService.Update(item);
await GetTreeListData();
}
async Task CreateItem(TreeListCommandEventArgs args)
{
var item = args.Item as Employee;
var parentItem = args.ParentItem as Employee;
await MyService.Create(item, parentItem);
await GetTreeListData();
}
async Task DeleteItem(TreeListCommandEventArgs args)
{
var item = args.Item as Employee;
await MyService.Delete(item);
await GetTreeListData();
}
async Task OnCancelHandler(TreeListCommandEventArgs args)
{
Employee empl = args.Item as Employee;
}
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; }
public override bool Equals(object obj)
{
if (obj is Employee)
{
return this.Id == (obj as Employee).Id;
}
return false;
}
}
async Task GetTreeListData()
{
Data = await MyService.Read();
}
protected override async Task OnInitializedAsync()
{
await GetTreeListData();
}
public static class MyService
{
private static List<Employee> _data { get; set; } = new List<Employee>();
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);
}
}
}
}
Example
The example below shows how you can handle the events the treelist exposes, so you can Create, Update or Delete records in your data source and the view model.
The treelist events use
EventCallback
and can be synchronous or asynchronous. The example below shows async versions, and the signature for synchronous events isvoid <MethodName>(TreeListCommandEventArgs args)
.
@using System.ComponentModel.DataAnnotations
@* Used for the model annotations only *@
Editing is cancelled for the first record.
<br />
<TelerikTreeList Data="@Data"
EditMode="@TreeListEditMode.Inline"
OnAdd="@AddItem"
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="280px">
<TreeListCommandButton Command="Add" Icon="@SvgIcon.Plus">Add Child</TreeListCommandButton>
<TreeListCommandButton Command="Edit" Icon="@SvgIcon.Pencil">Edit</TreeListCommandButton>
<TreeListCommandButton Command="Delete" Icon="@SvgIcon.Trash">Delete</TreeListCommandButton>
<TreeListCommandButton Command="Save" Icon="@SvgIcon.Save" ShowInEdit="true">Update</TreeListCommandButton>
<TreeListCommandButton Command="Cancel" Icon="@SvgIcon.Cancel" ShowInEdit="true">Cancel</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>
@logger
@code {
public List<Employee> Data { get; set; }
// Sample CUD operations for the local data
async Task AddItem(TreeListCommandEventArgs args)
{
//Set default values for new items
((Employee)args.Item).Name = "New Employee Name";
//Cancel if needed
//args.IsCancelled = true;
}
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();
AppendToLog("Update", args);
}
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();
AppendToLog("Create", args);
}
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();
AppendToLog("Delete", args);
}
// 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");
}
AppendToLog("Edit", args);
}
// OnCancel handler
async Task OnCancelHandler(TreeListCommandEventArgs args)
{
Employee empl = args.Item as Employee;
// if necessary, perform actual data source operation here through your service
AppendToLog("Cancel", args);
}
// sample visualization of the results
MarkupString logger;
void AppendToLog(string commandName, TreeListCommandEventArgs args)
{
string currAction = string.Format(
"<br />Command: <strong>{0}</strong>; is cancelled: <strong>{1}</strong>; is the item new: <strong>{2}</strong>",
commandName,
args.IsCancelled,
args.IsNew
);
logger = new MarkupString(logger + currAction);
}
// 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);
}
}
}
}
Notes
There are a few considerations to keep in mind with the CUD operations of the treelist. They are explained in the following list:
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.
-
The CRUD event handlers must be
async Task
and notasync void
. A Task can be properly awaited and allows working with services and contexts, and lets the treelist update after the actual data source operations complete.- When the method returns
void
, the execution of the context operations is not actually awaited, and you may get errors from the context (such as "Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application" or "A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext"). The treelist may also re-render before the actual data update happens and you may not see the result.
- When the method returns
The treelist uses
Activator.CreateInstance<TItem>();
to generate a new item when an Insert or Edit action is invoked, so the Model should have a parameterless constructor defined. If you cannot have such a constructor, you must use the OnModelInit event.While editing, the Treelist creates a copy of your original object which has a different reference. You receive that copy in the
OnUpdate
event handler. TheOnEdit
event receives the original item from the pristineData
collection, because it is a cancellable event and fires before the treelist logic creates the copy. The built-in editors and editor templates receive the copy for theircontext
that the treelist will create afterOnEdit
.The validation the treelist provides is based on the
DataAnnotationValidator
and creates its ownEditContext
for a row that is in edit/insert mode. When the row is not in edit/insert mode, theEditContext
isnull
. TheEditContext
is a cascading parameter and overrides any cascading parameters from parent components (such as an<EditForm>
that may wrap the treelist).When an input receives an
EditContext
(usually comes down as a cascading parameter), the framework also requires aValueExpression
. If you use two-way binding (the@bind-Value
syntax), theValueExpression
is deducted from there. However, if you use only theValue
property, you have to pass theValueExpression
yourself. This is a lambda expression that tells the framework what field in the model to update. The following sample demonstrates how to achieve that. You can also check the Requires a value for ValueExpression knowledge base article for more details.
<EditorTemplate>
<TelerikTextBox Value="@myModel.MyField"
ValueExpression="@( () => myModel.MyField )">
</TelerikTextBox>
</EditorTemplate>
@* Applies to the other input type components as well *@
-
If you want to perform other data operations while the component is in Edit mode (applicable for InCell and Inline editing) the following behavior will occur:
-
For operations like Filter, Group, Sort, Page, Search, Select, Row drag and Delete:
- InCell edit - if the validation is satisfied, a save operation will be executed. If the validation is not satisfied, editing will be cancelled, the
OnCancel
event will fire and the new user operation will be executed. In this case, you can handle the OnCancel event and set theIsCancelled
property of theCommandEventArgs
to true. Thus, the other data operation will be aborted and the Grid will remain in edit mode. - Inline edit - regardless of the validation, editing will be cancelled, the
OnCancel
event will fire and the new user operation will be executed. In this case, you can handle the OnCancel event and set theIsCancelled
property of theCommandEventArgs
to true. Thus, the other data operation will be aborted and the Grid will remain in edit mode.
- InCell edit - if the validation is satisfied, a save operation will be executed. If the validation is not satisfied, editing will be cancelled, the
-
For operations like Edit, Add, Save:
- InCell edit - if the validation is satisfied, the currently edited item will be saved and the command will be executed. If the validation is not satisfied, the command will be blocked until the item is valid or editing is cancelled.
- Inline edit - if the validation is satisfied,
OnCancel
will be fired for the currently edited item and the command will be executed. If the validation is not satisfied, the command will be blocked until the item is valid or editing is cancelled.
-