Bind Blazor TreeList to Data
- 23 minutes to read
This document describes how to bind the Blazor TreeList component to data in different scenarios.
Flat Data
You can bind the TreeList component to a flat data source that implements the IEnumerable<T> or IListSource interface. To build a hierarchical structure of TreeList nodes, specify additional properties that define node relationships:
- KeyFieldName
- A field name that contains node unique identifiers.
- ParentKeyFieldName
- A field name that contains a node’s parent identifier. This field’s value is
0
ornull
for root nodes.
Follow the steps below to bind the TreeList component to a data source that contains flat data:
- Bind the Data parameter to a C# property.
- Populate this property with data in the OnInitialized or OnInitializedAsync lifecycle method.
- Assign names of data source fields to the KeyFieldName and ParentKeyFieldName properties.
The following example binds DxTreeList
to a flat data source:
@inject EmployeeTaskService EmployeeTaskService
<DxTreeList Data="TreeListData" KeyFieldName="Id" ParentKeyFieldName="ParentId">
<Columns>
<DxTreeListDataColumn FieldName="Name" Caption="Task" />
<DxTreeListDataColumn FieldName="EmployeeName" />
<DxTreeListDataColumn FieldName="StartDate" />
<DxTreeListDataColumn FieldName="DueDate" />
</Columns>
</DxTreeList>
@code {
List<EmployeeTask> TreeListData { get; set; }
protected override void OnInitialized() {
TreeListData = EmployeeTaskService.GenerateData();
}
}
Hierarchical Data
You can bind the TreeList component to hierarchical data structures that implement the IEnumerable<T> interface.
Specify Node Children Declaratively
If each node in your data source includes a field with a list of child nodes, follow the steps below:
- Bind the Data parameter to a C# property.
- Populate this property with data in the OnInitialized or OnInitializedAsync lifecycle method.
- Assign the field name that stores children to the ChildrenFieldName property.
The following example binds the TreeList component to the SpaceObject
collection. An object’s Satellites
property stores child items.
@inject SpaceObjectDataProvider SpaceObjectDataProvider
<DxTreeList Data="TreeListData" ChildrenFieldName="Satellites">
<Columns>
<DxTreeListDataColumn FieldName="Name" />
<DxTreeListDataColumn FieldName="TypeOfObject" Caption="Type" />
<DxTreeListDataColumn FieldName="Mass10pow21kg" Caption="Mass, kg" DisplayFormat="N2">
<HeaderCaptionTemplate>Mass, 10<sup>21</sup> × kg</HeaderCaptionTemplate>
</DxTreeListDataColumn>
<DxTreeListDataColumn FieldName="MeanRadiusInKM" Caption="Radius, km" DisplayFormat="N2"/>
<DxTreeListDataColumn FieldName="Volume10pow9KM3" DisplayFormat="N2">
<HeaderCaptionTemplate>Volume, 10<sup>9</sup> × km<sup>3</sup></HeaderCaptionTemplate>
</DxTreeListDataColumn>
<DxTreeListDataColumn FieldName="SurfaceGravity" DisplayFormat="N2">
<HeaderCaptionTemplate>Gravity, m/s<sup>2</sup></HeaderCaptionTemplate>
</DxTreeListDataColumn>
</Columns>
</DxTreeList>
@code {
object TreeListData { get; set; }
protected override async Task OnInitializedAsync() {
TreeListData = SpaceObjectDataProvider.GenerateData();
}
}
Load Children in Code
If your data source does not include a field that stores child nodes, you can populate the TreeList component with data during component initialization. Follow the steps below to populate the component with data in code:
- Bind the Data parameter to a C# property.
- Populate this property with root data items in the OnInitialized or OnInitializedAsync lifecycle method.
- Handle the ChildrenLoading event. In the event handler, use the Parent event argument to determine the processed node and assign the node’s children to the Children collection.
Note
The TreeList component loads all data during the component initialization when you handle the ChildrenLoading event. Instead of this event, you can handle the ChildrenLoadingOnDemand event to load node children when the node is expanded for the first time. Refer to the following section for more information: Load Data on Demand.
The following example populates the TreeList component with data during the component initialization:
@inject SpaceObjectDataProvider SpaceObjectDataProvider
<DxTreeList Data="TreeListData" ChildrenLoading="OnChildrenLoading">
<Columns>
<DxTreeListDataColumn FieldName="Name" />
<DxTreeListDataColumn FieldName="TypeOfObject" Caption="Type" />
<DxTreeListDataColumn FieldName="Mass10pow21kg" Caption="Mass, kg" DisplayFormat="N2">
<HeaderCaptionTemplate>Mass, 10<sup>21</sup> × kg</HeaderCaptionTemplate>
</DxTreeListDataColumn>
<DxTreeListDataColumn FieldName="MeanRadiusInKM" Caption="Radius, km" DisplayFormat="N2"/>
<DxTreeListDataColumn FieldName="Volume10pow9KM3" DisplayFormat="N2">
<HeaderCaptionTemplate>Volume, 10<sup>9</sup> × km<sup>3</sup></HeaderCaptionTemplate>
</DxTreeListDataColumn>
<DxTreeListDataColumn FieldName="SurfaceGravity" DisplayFormat="N2">
<HeaderCaptionTemplate>Gravity, m/s<sup>2</sup></HeaderCaptionTemplate>
</DxTreeListDataColumn>
</Columns>
</DxTreeList>
@code {
object TreeListData { get; set; }
protected override async Task OnInitializedAsync() {
TreeListData = SpaceObjectDataProvider.GenerateRootData();
}
void OnChildrenLoading(TreeListChildrenLoadingEventArgs e) {
e.Children = SpaceObjectDataProvider.GetChildren((SpaceObject)e.Parent);
}
}
Observable Data Collections
You can bind the Blazor TreeList to a data collection that implements the INotifyCollectionChanged or IBindingList interface. For instance, you can use the standard ObservableCollection<T> or BindingList<T> objects. These collections notify the TreeList about relevant changes (when items are added or removed, when the entire collection is refreshed, and so on). The TreeList listens to these notifications and updates its data automatically.
To enable automatic data updates for individual cells, the item object should also implement the INotifyPropertyChanged interface.
Note
After you bind a dynamic data collection to the TreeList component, the collection sends notifications after each change. When you make a sequence of modifications (for instance, in a for
loop), the TreeList is re-rendered on each notification.
Based on the collection’s structure, follow the steps described in the Flat Data or Hierarchical Data section to bind the TreeList component to this collection. The following code sample binds the component to an ObservableCollection
that has a flat structure. The Update Progress button changes Status
field values:
@using Observable.Services
@using System.Collections.ObjectModel
@inject EmployeeTaskService EmployeeTaskService
<DxTreeList Data="TreeListData" KeyFieldName="Id" ParentKeyFieldName="ParentId">
<Columns>
<DxTreeListDataColumn FieldName="Name" Caption="Task" />
<DxTreeListDataColumn FieldName="EmployeeName" />
<DxTreeListDataColumn FieldName="StartDate" />
<DxTreeListDataColumn FieldName="DueDate" />
<DxTreeListDataColumn FieldName="Status" Caption="Progress" DisplayFormat="p0" />
</Columns>
</DxTreeList>
<DxButton Click="OnClick" Text="Update Progress" />
@code {
ObservableCollection<EmployeeTask> TreeListData { get; set; }
Random fixRand;
protected override void OnInitialized() {
TreeListData = new ObservableCollection<EmployeeTask>(EmployeeTaskService.GenerateData());
fixRand = new Random();
}
void OnClick() {
foreach (EmployeeTask task in TreeListData) {
if(task.Status != 1) {
double change = fixRand.NextDouble();
task.Status = (task.Status + change > 1) ? 1 : task.Status + change;
}
}
}
}
Server-Side Data
You can use the GridDevExtremeDataSource<T> to bind the TreeList to a large flat dataset that implements the IQueryable interface. To build a hierarchical structure of TreeList nodes, specify additional properties that define node relationship:
- KeyFieldName
- A field name that contains node unique identifiers.
- ParentKeyFieldName
- A field name that contains a node’s parent identifier. This field’s value is
0
ornull
for root nodes. - HasChildrenFieldName
- A field name that returns whether a node has children. The component uses this property to determine which nodes require expand buttons.
When you use this data source, the TreeList delegates data filtering operations to an underlying query provider (such as LINQ to Objects, EF Core, and so on). The TreeList initially loads only root nodes and loads a node’s children when the node is expanded for the first time. This technique optimizes overall performance and reduces memory consumption.
Data source implementation is based on the DevExpress DevExtreme.AspNet.Data library.
Local Queryable Collections
Follow the steps below to bind the TreeList to a large flat dataset stored locally:
- Create a
GridDevExtremeDataSource
instance and pass your IQueryable data collection as the constructor parameter. - Assign this instance to the TreeList’s Data property in the OnInitialized or OnInitializedAsync lifecycle method.
- Specify KeyFieldName, ParentKeyFieldName, and HasChildrenFieldName properties.
The following code sample example uses the Entity Framework Core data access technology to bind the TreeList component to an IQueryable<T>
data collection:
@inject CitiesService CitiesService
<DxTreeList Data="@Data"
KeyFieldName="ID"
ParentKeyFieldName="ParentID"
HasChildrenFieldName="HasChildren">
<Columns>
<DxTreeListDataColumn Caption="Location" FieldName="Name" />
<DxTreeListDataColumn FieldName="CityType" />
<DxTreeListDataColumn FieldName="Year" DisplayFormat="d"/>
<DxTreeListDataColumn FieldName="RecordType" />
<DxTreeListDataColumn FieldName="Population" />
</Columns>
</DxTreeList>
@code {
object Data { get; set; }
protected override async Task OnInitializedAsync() {
var cities = await CitiesService.GetCitiesAsync();
Data = new GridDevExtremeDataSource<Location>(cities.AsQueryable());
}
}
Queryable Collections as HTTP Services
Follow these steps to bind the TreeList to a large data collection published as an HTTP service:
Create a
GridDevExtremeDataSource
class instance and pass two constructor parameters:- An HttpClient object that sends HTTP requests and receives HTTP responses from the specified URL.
- A URL to the service’s controller action that processes HTTP requests (see step 3).
Assign the
GridDevExtremeDataSource
class instance to the TreeList’s Data property in the OnInitialized or OnInitializedAsync lifecycle method.- On the service side, implement an API controller. Create action methods that use the DevExtreme.AspNet.Data library’s DataSourceLoader class to create a
LoadResult
object based on load options. - Specify KeyFieldName, ParentKeyFieldName, and HasChildrenFieldName properties.
@inject HttpClient HttpClient
<DxTreeList Data="Data"
KeyFieldName="Id"
ParentKeyFieldName="ParentId"
HasChildrenFieldName="HasChildren">
<Columns>
<DxTreeListDataColumn FieldName="Name" Caption="Task"/>
<DxTreeListDataColumn FieldName="EmployeeName" Caption="Assigned To"/>
<DxTreeListDataColumn FieldName="StartDate" />
<DxTreeListDataColumn FieldName="DueDate" />
<DxTreeListDataColumn FieldName="Priority" />
<DxTreeListDataColumn Caption="Progress" FieldName="Status" />
</Columns>
</DxTreeList>
@code {
object Data { get; set; }
protected override async Task OnInitializedAsync() {
var uri = new Uri("https://js.devexpress.com/Demos/NetCore/api/TreeListWebApi/EmployeeTasks");
Data = new GridDevExtremeDataSource<EmployeeTask>(HttpClient, uri);
}
}
Custom HTTP Requests
You can also generate and send custom HTTP requests:
Implement an asynchronous function that returns a
Task<Stream>
object and accepts two parameters:- A URL to the service’s controller action.
- A CancellationToken object that propagates a cancellation notification.
Create a
GridDevExtremeDataSource
class instance and pass two constructor parameters (the newly created function and the URL to the service’s controller action).- Assign the
GridDevExtremeDataSource
instance to the TreeList’s Data property in the OnInitialized or OnInitializedAsync lifecycle method. - Specify KeyFieldName, ParentKeyFieldName, and HasChildrenFieldName properties.
The following code snippet adds an authorization header to HTTP requests. Note that you should process authorization information in the service’s controller.
@*...*@
@code {
// ...
protected override async Task OnInitializedAsync() {
var uri = new Uri("https://js.devexpress.com/Demos/NetCore/api/TreeListWebApi/EmployeeTasks");
Data = new GridDevExtremeBasedDataSource<EmployeeTask>(ExecuteDataSourceHttpRequest, uri);
}
async Task<Stream> ExecuteDataSourceHttpRequest(Uri uri, CancellationToken cancellationToken) {
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "...");
var response = await HttpClient.SendAsync(request);
return await response.Content.ReadAsStreamAsync();
}
}
Limitations
The GridDevExtremeDataSource
imposes the following limitations on TreeList features:
- Summary calculation is not supported.
- ExpandAll() and CollapseAll() methods are not supported.
- To improve performance, the TreeList component displays a maximum of 500 rows that match the current filter.
- Criteria operators applied by the SetFieldFilterCriteria(String, CriteriaOperator) or SetFilterCriteria(CriteriaOperator) methods are limited. For more information, see method descriptions.
AllPages
mode of the Select All checkbox is not supported.- The SelectAllAsync(Boolean) and DeselectAllAsync() methods load all data to the TreeList component and can reduce performance and increase memory consumption.
- The second call to the SelectAllAsync(Boolean) or DeselectAllAsync() method cancels the operation initiated by the previously called method.
- Sort and filter operations cancel incomplete “select all” and “deselect all” processes.
- Call the WaitForRemoteSourceRowLoadAsync method before those that accept a row’s visible index as a parameter (SelectRow, ExpandRow, and so on) to ensure that the specified data row is loaded.
Load Data on Demand
DevExpress Blazor TreeList can initially load only root nodes and retrieve the node’s children when the node is expanded for the first time. This technique optimizes performance and reduces overall memory consumption when bound to a large dataset. Follow the steps below to enable on demand mode in the TreeList component:
- Bind the Data parameter to a C# property.
- Populate this property with root data items in the OnInitialized or OnInitializedAsync lifecycle method.
- Specify the HasChildrenFieldName property. The component uses this property to determine which nodes require expand buttons.
- Handle the ChildrenLoadingOnDemand event. In the event handler, use the Parent event argument to determine the processed node and assign the node’s children to the Children collection.
In the following example, the TreeList component displays the file structure:
@inject IFileSystemDataProvider FileSystemDataProvider
<DxTreeList Data="TreeListData"
HasChildrenFieldName="HasChildren"
ChildrenLoadingOnDemand="TreeList_ChildrenLoadingOnDemand"
CustomizeCellDisplayText="TreeList_CustomizeCellDisplayText">
<Columns>
<DxTreeListDataColumn FieldName="Name" />
<DxTreeListDataColumn FieldName="Type" Width="100" />
<DxTreeListDataColumn FieldName="DateModified" Width="120" />
<DxTreeListDataColumn FieldName="Size" Width="100" />
</Columns>
</DxTreeList>
@code {
object TreeListData { get; set; }
protected override async Task OnInitializedAsync() {
TreeListData = await FileSystemDataProvider.GetRootItemsAsync();
}
Task TreeList_ChildrenLoadingOnDemand(TreeListChildrenLoadingOnDemandEventArgs e) {
var item = e.Parent as FileSystemDataItem;
e.Children = item.Children;
return Task.CompletedTask;
}
void TreeList_CustomizeCellDisplayText(TreeListCustomizeCellDisplayTextEventArgs e) {
if(e.FieldName == "Size") {
var item = (FileSystemDataItem)e.DataItem;
e.DisplayText = GetSizeColumnDisplayText(item.Type, item.Size);
}
}
string GetSizeColumnDisplayText(FileType fileType, long size) {
if(fileType == FileType.Folder)
return null;
if(size >= 1024)
return string.Format("{0:### ### ###} KB", size / 1024);
return string.Format("{0} Bytes", size);
}
}
Limitations
The on demand data loading mode has the following specifics and limitations:
- A filter operation forces the component to load all data.
- Summary calculation is not supported.
- ExpandAll() and CollapseAll() methods are not supported.
AllPages
mode of the Select All checkbox is not supported.- The SelectAllAsync and DeselectAllAsync method calls force the component to load all data.
- The second call to the SelectAllAsync or DeselectAllAsync method cancels the operation initiated by the previously called method.
- Sort and filter operations cancel incomplete “select all” and “deselect all” processes.
- Call the WaitForRemoteSourceRowLoadAsync method before those that accept a row’s visible index as a parameter (SelectRow, ExpandRow, and so on) to ensure that the specified data row is loaded.