6 Answers, 1 is accepted
Hello Ed,
On our Feedback Portal there is a thread for a Feature Request regarding this functionality. You can Follow the status updates for its implementation from this URL: https://feedback.telerik.com/blazor/1427582-select-and-multiple-select-of-items. I have given a Vote on your behalf already.
Also, there you can find a workaround solution for the time-being.
Regards,
Svetoslav Dimitrov
Progress Telerik

Try this:
@using System.Collections.ObjectModel
@using Palmer.Doc.Models
@page "/tree"
<
TelerikButton
Primary
=
"true"
OnClick
=
"CreateItem"
>Create item</
TelerikButton
>
<
TelerikButton
Primary
=
"true"
OnClick
=
"DeleteItem"
>Delete item</
TelerikButton
>
<
TelerikTreeView
Data
=
"@TreeData"
>
<
TreeViewBindings
>
<
TreeViewBinding
IdField
=
"Id"
TextField
=
"Text"
>
<
ItemTemplate
>
@{
<
TelerikButton
OnClick="@(() => OnClickHandler(context as TreeItem))">@((context as TreeItem).Text)</
TelerikButton
>
}
</
ItemTemplate
>
</
TreeViewBinding
>
</
TreeViewBindings
>
</
TelerikTreeView
>
@code{
public class TreeItem
{
public string Id { get; set; } = $"{Guid.NewGuid()}";
public string Text { get; set; }
public string ContextId { get; set; }
public ObservableCollection<
TreeItem
> Items { get; set; } = new ObservableCollection<
TreeItem
>();
public bool Expanded { get; set; }
public bool HasChildren { get; set; }
public bool Selected { get; set; }
}
#region Properties
public TreeItem SelectedItem { get; set; }
public ObservableCollection<
TreeItem
> TreeData { get; set; }
#endregion
#region Event Handlers
protected override void OnInitialized()
{
LoadHierarchical();
}
private void OnClickHandler(TreeItem context)
{
SelectedItem = context;
}
#endregion
#region Methods
private void CreateItem()
{
if (SelectedItem != null)
{
AddNode(SelectedItem, "New Item");
}
}
private void DeleteItem()
{
if (SelectedItem != null)
{
TreeItem parent = GetById(SelectedItem.ContextId);
if (parent != null)
{
parent.Items.Remove(SelectedItem);
SelectedItem = parent;
SelectedItem.HasChildren = SelectedItem.Items.Count > 0;
SelectedItem.Expanded = SelectedItem.HasChildren;
}
}
}
private void LoadHierarchical()
{
ObservableCollection<
TreeItem
> roots =
new ObservableCollection<
TreeItem
>()
{
new TreeItem { Text = "Item 1", Expanded = true },
new TreeItem { Text = "Item 2" }
};
AddNode(roots[0], "Item 1 first child");
AddNode(roots[0], "Item 1 second child");
AddNode(roots[1], "Item 2 first child");
AddNode(roots[1], "Item 2 second child");
TreeData = roots;
}
private void AddNode(TreeItem parent, string childText)
{
AddNode(parent, new TreeItem()
{
Text = childText
});
}
private void AddNode(TreeItem context, TreeItem child)
{
child.ContextId = context.Id;
context.Items.Add(child);
context.HasChildren = true;
context.Expanded = true;
SelectedItem = child;
}
public TreeItem GetById(string id)
{
TreeItem result = null;
// perform a recursive search starting with the root nodes
foreach (var treeItem in TreeData)
{
result = getById(id, treeItem);
if (result != null) break;
}
return result;
}
/// <
summary
>
/// Perform a recursive search using the given node
/// </
summary
>
/// <
param
name
=
"id"
></
param
>
/// <
param
name
=
"parent"
></
param
>
/// <
returns
></
returns
>
private TreeItem getById(string id, TreeItem node)
{
TreeItem result = null;
if (node.Id == id)
{
result = node;
}
else
{
foreach (TreeItem child in node.Items)
{
result = getById(id, child);
if (result != null) break;
}
}
return result;
}
#endregion
}

Thanks to all who responded to my query. Based on what I have learned I thought I would share my version. It has a feature that acts as a selected node so that when you click on a node it stays selected until you click on another node. Hope someone finds it useful. It's not perfect, but seems to do the job.
Ed
@page "/CategoryTree"
@using Telerik.Blazor.Components
@using AlderneyTreasures.Data
@using Telerik.Blazor.Components.Common.Editors
@using Telerik.DataSource.Extensions
@using Microsoft.EntityFrameworkCore;
@using System.Collections.ObjectModel
@inject ATDBContext db
@inject Blazored.Toast.Services.IToastService toastService;
<
TelerikTreeView
@
ref
=
"tv"
Data
=
"@tvData"
OnExpand
=
"@LoadChildren"
>
<
TreeViewBindings
>
<
TreeViewBinding
TextField
=
"ItemName"
ItemsField
=
"Items"
IconField
=
"Icon"
>
<
ItemTemplate
Context
=
"ctx"
>
@{
TvItem item = ctx as TvItem;
<
span
@onclick="@( _ => OnNodeClick(item) )" style="@(item.NodeBackColor)">
<
img
src
=
"/images/folder.png"
/> <
strong
>@item.ItemName</
strong
>
</
span
>
}
</
ItemTemplate
>
</
TreeViewBinding
>
<
TreeViewBinding
Level
=
"1"
TextField
=
"ItemName"
ItemsField
=
"Items"
IconField
=
"Icon"
>
<
ItemTemplate
Context
=
"ctx"
>
@{
TvItem item = ctx as TvItem;
<
span
@onclick="@( _ => OnNodeClick(item) )" style="@(item.NodeBackColor)">
<
img
src
=
"/images/folder.png"
/> <
strong
>@item.ItemName</
strong
>
</
span
>
}
</
ItemTemplate
>
</
TreeViewBinding
>
</
TreeViewBindings
>
</
TelerikTreeView
>
@code {
private int categoryId;
[Parameter] public int CategoryId
{
get { return categoryId; }
set
{
categoryId = value;
}
}
[Parameter]
public EventCallback<
int
> CategoryIdChanged { get; set; }
public bool ShowInactive { get; set; } = false;
TelerikTreeView tv { get; set; }
public ObservableCollection<
TvItem
> tvData { get; set; }
public class TvItem
{
public string ItemName { get; set; }
public string Icon { get; set; }
public int ItemId { get; set; } //will be used to identify the node, not for rendering in this example
public int? ParentItemId { get; set; }
public ObservableCollection<
TvItem
> Items { get; set; }
public bool Expanded { get; set; }
public bool HasChildren { get; set; }
public bool IsActive { get; set; }
public bool Selected { get; set; }
public int Level { get; set; }
public string NodeBackColor { get; set; }
}
public TvItem SelectedNode { get; set; }
public TvItem PrevSelectedNode { get; set; }
private void OnNodeClick(TvItem item)
{
if (SelectedNode != null)
{
PrevSelectedNode = SelectedNode;
PrevSelectedNode.NodeBackColor = "background-color:white;";
}
SelectedNode = item;
SelectedNode.NodeBackColor = "background-color:lightpink;";
}
protected override void OnInitialized()
{
}
protected override void OnParametersSet()
{
base.OnParametersSet();
LoadtvData();
OnNodeClick(FindNode(tvData, CategoryId));
}
public void LoadtvData()
{
var q = from a in db.Categories
where a.ParentCategoryId == null
orderby a.CategoryName
select new TvItem()
{
IsActive = (bool)a.IsActive,
ItemName = a.CategoryName,
ItemId = a.Id,
Icon = "folder",
//ParentId = null,
HasChildren = (from b in db.Categories
where b.ParentCategoryId == a.Id
select b).Any(),
//Expanded = false,
Level = 1
};
if (!ShowInactive)
{
q = q.Where(a => a.IsActive == true);
}
tvData = new ObservableCollection<
TvItem
>(q.ToList());
foreach (var item in tvData)
{
if (item.HasChildren == true)
item.Icon = "folder";
}
}
private async Task LoadChildren(TreeViewExpandEventArgs args)
{
// check if the item is expanding, we don't need to do anything if it is collapsing
// in this example we will also check the type of the model to know how to identify the node and what data to load. If you use only one model for all levels, you don't have to do this
if (args.Expanded && args.Item is TvItem)
{
TvItem currItem = args.Item as TvItem;
if (currItem.Items?.Count > 0)
{
return; // item has been expanded before so it has data, don't load data again
// alternatively, load it again but make sure to handle the child items correctly
// either overwrite the entire collection, or use some other logic to append/merge
}
int itemIdentifier = currItem.ItemId;
if (currItem.HasChildren == false)
{
StateHasChanged(); // inform the UI that the data is changed
return;
}
var q = from a in db.Categories
where a.ParentCategoryId == currItem.ItemId
orderby a.CategoryName
select new TvItem()
{
IsActive = (bool)a.IsActive,
ItemName = a.CategoryName,
ItemId = a.Id,
ParentItemId = currItem.ItemId,
HasChildren = (from b in db.Categories
where b.ParentCategoryId == a.Id
select b).Any(),
Icon = a.Icon,
Expanded = false,
Level = currItem.Level + 1
};
if (!ShowInactive)
{
q = q.Where(a => a.IsActive == true);
}
currItem.Items = new ObservableCollection<
TvItem
>(q.ToList());
foreach (var item in currItem.Items)
{
if (item.HasChildren == true)
item.Icon = "folder";
}
StateHasChanged(); // inform the UI that the data is changed
}
}
public TvItem FindNode(ObservableCollection<
TvItem
> data, int catId)
{
foreach (TvItem item in data)
{
if (item.ItemId == catId)
return item;
else
{
if (item.Items != null)
{
foreach (TvItem subcat in item.Items)
{
if (subcat.ItemId == catId)
return subcat;
else
{
if (subcat.Items != null)
{
return FindNode(subcat.Items, catId);
}
}
}
}
}
}
return null;
}
}

Minor bug fix
@page "/CategoryTree"
@using Telerik.Blazor.Components
@using AlderneyTreasures.Data
@using Telerik.Blazor.Components.Common.Editors
@using Telerik.DataSource.Extensions
@using Microsoft.EntityFrameworkCore;
@using System.Collections.ObjectModel
@inject ATDBContext db
@inject Blazored.Toast.Services.IToastService toastService;
<
TelerikTreeView
@
ref
=
"tv"
Data
=
"@tvData"
OnExpand
=
"@LoadChildren"
>
<
TreeViewBindings
>
<
TreeViewBinding
TextField
=
"CategoryName"
ItemsField
=
"Categories"
IconField
=
"Icon"
>
<
ItemTemplate
Context
=
"ctx"
>
@{
CategoryItem item = ctx as CategoryItem;
<
span
@onclick="@( _ => OnNodeClick(item) )" style="@(item.NodeBackColor)">
<
img
src
=
"/images/folder.png"
/> <
strong
>@item.CategoryName</
strong
>
</
span
>
}
</
ItemTemplate
>
</
TreeViewBinding
>
<
TreeViewBinding
Level
=
"1"
TextField
=
"CategoryName"
ItemsField
=
"Categories"
IconField
=
"Icon"
>
<
ItemTemplate
Context
=
"ctx"
>
@{
CategoryItem item = ctx as CategoryItem;
<
span
@onclick="@( _ => OnNodeClick(item) )" style="@(item.NodeBackColor)">
<
img
src
=
"/images/folder.png"
/> <
strong
>@item.CategoryName</
strong
>
</
span
>
}
</
ItemTemplate
>
</
TreeViewBinding
>
</
TreeViewBindings
>
</
TelerikTreeView
>
@code {
private int categoryId;
[Parameter] public int CategoryId
{
get { return categoryId; }
set
{
categoryId = value;
}
}
[Parameter]
public EventCallback<
int
> CategoryIdChanged { get; set; }
public bool ShowInactive { get; set; } = false;
TelerikTreeView tv { get; set; }
public ObservableCollection<
CategoryItem
> tvData { get; set; }
public class CategoryItem
{
public string CategoryName { get; set; }
public string Icon { get; set; }
public int ItemId { get; set; } //will be used to identify the node, not for rendering in this example
public int? ParentItemId { get; set; }
public ObservableCollection<
CategoryItem
> Categories { get; set; }
public bool Expanded { get; set; }
public bool HasChildren { get; set; }
public bool IsActive { get; set; }
public bool Selected { get; set; }
public int Level { get; set; }
public string NodeBackColor { get; set; }
}
public CategoryItem SelectedNode { get; set; }
public CategoryItem PrevSelectedNode { get; set; }
private void OnNodeClick(CategoryItem item)
{
if (SelectedNode != null)
{
PrevSelectedNode = SelectedNode;
PrevSelectedNode.NodeBackColor = "background-color:white;";
}
if (item != null)
{
SelectedNode = item;
SelectedNode.NodeBackColor = "background-color:lightpink;";
}
}
protected override void OnInitialized()
{
}
protected override void OnParametersSet()
{
base.OnParametersSet();
LoadtvData();
OnNodeClick(FindNode(tvData, CategoryId));
}
public void LoadtvData()
{
var q = from a in db.Categories
where a.ParentCategoryId == null
orderby a.CategoryName
select new CategoryItem()
{
IsActive = (bool)a.IsActive,
CategoryName = a.CategoryName,
ItemId = a.Id,
Icon = "folder",
//ParentId = null,
HasChildren = (from b in db.Categories
where b.ParentCategoryId == a.Id
select b).Any(),
//Expanded = false,
Level = 1
};
if (!ShowInactive)
{
q = q.Where(a => a.IsActive == true);
}
tvData = new ObservableCollection<
CategoryItem
>(q.ToList());
foreach (var item in tvData)
{
if (item.HasChildren == true)
item.Icon = "folder";
}
}
private async Task LoadChildren(TreeViewExpandEventArgs args)
{
// check if the item is expanding, we don't need to do anything if it is collapsing
// in this example we will also check the type of the model to know how to identify the node and what data to load. If you use only one model for all levels, you don't have to do this
if (args.Expanded && args.Item is CategoryItem)
{
CategoryItem currItem = args.Item as CategoryItem;
if (currItem.Categories?.Count > 0)
{
return; // item has been expanded before so it has data, don't load data again
// alternatively, load it again but make sure to handle the child items correctly
// either overwrite the entire collection, or use some other logic to append/merge
}
int itemIdentifier = currItem.ItemId;
if (currItem.HasChildren == false)
{
StateHasChanged(); // inform the UI that the data is changed
return;
}
var q = from a in db.Categories
where a.ParentCategoryId == currItem.ItemId
orderby a.CategoryName
select new CategoryItem()
{
IsActive = (bool)a.IsActive,
CategoryName = a.CategoryName,
ItemId = a.Id,
ParentItemId = currItem.ItemId,
HasChildren = (from b in db.Categories
where b.ParentCategoryId == a.Id
select b).Any(),
Icon = a.Icon,
Expanded = false,
Level = currItem.Level + 1
};
if (!ShowInactive)
{
q = q.Where(a => a.IsActive == true);
}
currItem.Categories = new ObservableCollection<
CategoryItem
>(q.ToList());
foreach (var item in currItem.Categories)
{
if (item.HasChildren == true)
item.Icon = "folder";
}
StateHasChanged(); // inform the UI that the data is changed
}
}
public CategoryItem FindNode(ObservableCollection<
CategoryItem
> data, int catId)
{
foreach (CategoryItem item in data)
{
if (item.ItemId == catId)
return item;
else
{
if (item.Categories != null)
{
foreach (CategoryItem subcat in item.Categories)
{
if (subcat.ItemId == catId)
return subcat;
else
{
if (subcat.Categories != null)
{
return FindNode(subcat.Categories, catId);
}
}
}
}
}
}
return null;
}
}