Checking a checkbox of a on demand-loaded child then collapsing and reopening its parent makes the checkbox disappear. However, it is still checked in the CheckedItems collection, but just not in the UI. See this REPL example. Steps...
1. Expand a top level item
2. Check its child checkbox
3. Collapse the top level item
4. Expand it again
Result: checkbox gone (in the UI)
The problem at hand arises when attempting to update the CheckedItems property of a TreeView control from within an async method.
The problem seems to be timing-related and is not always reproducible. The issue is observed most often when starting the project. It seems to be reproducible only in Blazor Server App.
Reproduction:
To reproduce the issue, try running the following snippet in a Blazor Server App.
@page "/"
<TelerikTreeView Data="@FlatData"
CheckBoxMode="@TreeViewCheckBoxMode.Multiple"
CheckParents="@true"
CheckChildren="@true"
CheckOnClick="@false"
@bind-CheckedItems="@SelectedItems">
</TelerikTreeView>
@code {
List<TreeItem> FlatData { get; set; }
IEnumerable<object> SelectedItems { get; set; } = new List<object>();
protected override async Task OnInitializedAsync()
{
await GenerateData();
await SelectDefault();
}
async Task SelectDefault()
{
await Task.Delay(100);
SelectedItems = FlatData.Where(data => data.Id == 2);
}
#pragma warning disable
async Task GenerateData()
{
FlatData = new List<TreeItem>();
FlatData.Add(new TreeItem()
{
Id = 1,
Text = "Project",
ParentId = null,
HasChildren = true,
Icon = "folder",
Expanded = true
});
FlatData.Add(new TreeItem()
{
Id = 2,
Text = "Design",
ParentId = 1,
HasChildren = true,
Icon = "brush",
Expanded = true
});
FlatData.Add(new TreeItem()
{
Id = 3,
Text = "Implementation",
ParentId = 1,
HasChildren = true,
Icon = "folder",
Expanded = true
});
}
public class TreeItem
{
public int Id { get; set; }
public string Text { get; set; }
public int? ParentId { get; set; }
public bool HasChildren { get; set; }
public string Icon { get; set; }
public bool Expanded { get; set; }
}
}
The second TreeItem should be selected.
The TreeView resets the checkbox state of its items when the app adds or removes a node to the component datasource.
A possible workaround is to reset the object reference of the CheckedItems parameter.
The following test page with a workaround is a modified version of TreeView - Refresh Data - Observable Data.
@using System.Collections.ObjectModel
<TelerikButton OnClick="@AddItem">Add Item</TelerikButton>
<TelerikButton OnClick="@RemoveItem">Remove Item</TelerikButton>
<p>
TreeViewCheckedItems Count: @TreeViewCheckedItems.Count()
<br />
<label><TelerikCheckBox @bind-Value="@ApplyWorkaround" /> Apply Workaround</label>
</p>
<TelerikTreeView Data="@TreeViewData"
CheckBoxMode="@TreeViewCheckBoxMode.Multiple"
@bind-CheckedItems="@TreeViewCheckedItems" />
@code {
private IEnumerable<object> TreeViewCheckedItems { get; set; } = new List<TreeItem>();
private ObservableCollection<object> TreeViewData { get; set; } = new ObservableCollection<object>();
private int LastId { get; set; }
private bool ApplyWorkaround { get; set; }
private void AddItem()
{
TreeViewData.Add(
new TreeItem
{
Id = ++LastId,
Text = $"Item {LastId}",
});
if (ApplyWorkaround)
{
TreeViewCheckedItems = new List<object>(TreeViewCheckedItems);
}
}
private void RemoveItem()
{
if (TreeViewData.Count > 1)
{
TreeViewData.Remove(TreeViewData.Last());
if (ApplyWorkaround)
{
TreeViewCheckedItems = new List<object>(TreeViewCheckedItems);
}
}
}
protected override void OnInitialized()
{
TreeViewData = new ObservableCollection<object>() {
new TreeItem()
{
Id = ++LastId,
Text = $"Item {LastId}"
}
};
TreeViewCheckedItems = new List<object>() { TreeViewData.First() };
}
public class TreeItem
{
public int Id { get; set; }
public int? ParentId { get; set; }
public string Text { get; set; } = string.Empty;
public bool HasChildren { get; set; }
}
}
asdf
When using load-on-demand, you may not know how much data there is and how many levels deep it will go. You need the OnExpand event to fire for each node so you can call your services and retrieve data.
Unfortunately, it does not fire for the second level of nodes, so you cannot load data for them.
@
using
Telerik.Blazor.Components.TreeView
<button
class
=
"btn btn-primary"
@onclick=
"@(() => ChangeTreeData(false))"
>Change tree data</button>
<TelerikTreeView Data=
"@FlatData"
>
<TelerikTreeViewBindings>
<TelerikTreeViewBinding ParentIdField=
"Parent"
ExpandedField=
"IsExpanded"
></TelerikTreeViewBinding>
</TelerikTreeViewBindings>
</TelerikTreeView>
@code {
public
List<TreeItem> FlatData {
get
;
set
; } =
new
List<TreeItem>();
public
class
TreeItem
//most fields use the default names and will bind automatically in this example
{
public
int
Id {
get
;
set
; }
public
string
Text {
get
;
set
; }
public
int
? Parent {
get
;
set
; }
//this is a non-default field name
public
bool
HasChildren {
get
;
set
; }
public
bool
IsExpanded {
get
;
set
; }
//this is a non-default field name
}
protected
override
void
OnInit()
{
ChangeTreeData(
true
);
}
int
currIndex {
get
;
set
; } = 0;
void
ChangeTreeData(
bool
expandItems)
{
currIndex++;
FlatData.Clear();
List<TreeItem> data =
new
List<TreeItem>();
data.Add(
new
TreeItem { Id = currIndex, HasChildren =
true
, IsExpanded = expandItems, Parent =
null
, Text = $
"root {currIndex}"
});
data.Add(
new
TreeItem { Id = currIndex + 1, HasChildren =
false
, IsExpanded = expandItems, Parent = currIndex, Text = $
"root {currIndex}: child one"
});
FlatData.AddRange(data);
}
}
Repro plus workaround (already in):
<button @onclick="@SwitchDataSource">switch to other data source</button>
<TelerikTreeView Data="@FlatData">
<TreeViewBindings>
<TreeViewBinding IdField="Id" ParentIdField="ParentIdValue" ExpandedField="Expanded" TextField="Text"
HasChildrenField="HasChildren" IconField="Icon">
<ItemTemplate>
@{
TreeItem currProduct = context as TreeItem;
@(currProduct.Text)
}
</ItemTemplate>
</TreeViewBinding>
</TreeViewBindings>
</TelerikTreeView>
@code {
public class TreeItem
{
public int Id { get; set; }
public string Text { get; set; }
public int? ParentIdValue { get; set; }
public bool HasChildren { get; set; }
public string Icon { get; set; }
public bool Expanded { get; set; }
}
async void SwitchDataSource()
{
//workaround - remove it to see the actual error
foreach (TreeItem item in FlatData)
{
item.Expanded = false;
}
StateHasChanged();
await Task.Delay(300);//awaits the animation that will hide the nodes we just collapsed so their elements get properly disposed
//change data
LoadSecondDataSource();
//update UI
StateHasChanged();
}
public IEnumerable<TreeItem> FlatData { get; set; }
protected override void OnInitialized()
{
LoadFlatData();
}
private void LoadFlatData()
{
List<TreeItem> items = new List<TreeItem>();
items.Add(new TreeItem()
{
Id = 1,
Text = "Project",
ParentIdValue = null,
HasChildren = true,
Icon = "folder",
Expanded = true
});
items.Add(new TreeItem()
{
Id = 2,
Text = "Design",
ParentIdValue = 1,
HasChildren = true,
Icon = "brush",
Expanded = true
});
items.Add(new TreeItem()
{
Id = 3,
Text = "Implementation",
ParentIdValue = 1,
HasChildren = true,
Icon = "folder",
Expanded = true
});
items.Add(new TreeItem()
{
Id = 4,
Text = "site.psd",
ParentIdValue = 2,
HasChildren = false,
Icon = "psd",
Expanded = true
});
items.Add(new TreeItem()
{
Id = 5,
Text = "index.js",
ParentIdValue = 3,
HasChildren = false,
Icon = "js"
});
items.Add(new TreeItem()
{
Id = 6,
Text = "index.html",
ParentIdValue = 3,
HasChildren = false,
Icon = "html"
});
items.Add(new TreeItem()
{
Id = 7,
Text = "styles.css",
ParentIdValue = 3,
HasChildren = false,
Icon = "css"
});
FlatData = items;
}
void LoadSecondDataSource()
{
List<TreeItem> items = new List<TreeItem>();
items.Add(new TreeItem()
{
Id = 1,
Text = "1",
ParentIdValue = null,
HasChildren = true
});
items.Add(new TreeItem()
{
Id = 2,
Text = "1 1",
ParentIdValue = 1,
HasChildren = true
});
//if you add this, there is no error because the levels match
//if the new data source does not have the same number of (maybe expanded) levels
//you will get an error while disposing those levels
//items.Add(new TreeItem()
//{
// Id = 3,
// Text = "1 1 1",
// ParentIdValue = 2
//});
FlatData = items;
}
}
We have the following scenario:
The problem is when you close the dialog before the loading is finished, a NullReferenceException occurs.
System.NullReferenceException: Object reference not set to an instance of an object.
at Telerik.Blazor.Components.Common.Trees.TelerikTreeDataSource.GetTreeItems(IEnumerable`1 data, Int32 level, String parentIndex)
I am trying to restore the expanded items by programmatically populating the ExpandedItems collection of the TreeView.
I have overriden "Equals" on the model, so the items are not compared by reference but by a unique identifier. The problem is that the TelerikTreeView does not respect this override and does not expand the items.
Other controls with similar features do respect overriden implementation of "Equals".
Reproduction: https://blazorrepl.telerik.com/cSuKkqwu46w4sHaW53.
There are two related issues in this bug report:
See REPL: https://blazorrepl.telerik.com/wwOHGPvi11wy1OBp06
Steps to reproduce:
Per WAI-ARIA 1.2, the "aria-level" attribute, if present, "is an integer greater than or equal to 1".
However, Blazor TreeView uses 0-indexing for the attribute which means that the root item has an invalid (and ignored) aria-level property. This is problematic because the browser will infer the level from the DOM nesting for the root item, but then use aria-level for all other sub-trees, leading to inconsistent levels (in the demo, the level goes 2 > 1 > 2).
The feature request is about introducing a configuration option about a delay to start the drag operation. Currently, the lack of such functionality causes unexpected drag clue appearance on double click.
```
In my treeview the user can double-click to open an item or they can drag to reorganize the tree. When you double-click you can see the drag & drop initiates for the brief fraction of a second. Which is a confusing and unpleasant UX. It would be preferable if a drag & drop didn't initiate until the user dragged for 3-4 pixels from the mousedown location, as opposed to a single pixel.
In general if there's a behavior like this that I would want to tweak, are there ways to adjust the control or override behaviors using Javascript? I used to do all sorts of stuff with the jQuery controls.
```
Steps to reproduce:
Expected: I should be able to clearly tell what element I'm currently focused on.
Actual: There is no focus indicator.