Display a Tree List With Flat Data Objects (ASP.NET Core Blazor)
- 6 minutes to read
ASP.NET Core Blazor DxTreeListEditor component can display a flat data object collection as a tree-like structure. To make this possible, objects in the data source should have relationships defined by Key and Parent Key properties. This topic shows an example of such implementation.
Note
In this scenario, classes do not need to implement the ITreeNode interface.
Initial Data Model Implementation
First, implement a simple class with a ParentObjectId property that targets a parent node’s key value.
HasChildren property allows you to use Queryable data access mode. In this mode, Tree List loads child nodes from the database only when you expand the parent node. The HasChildren property shows whether the parent object should contain children.
Note
- If you make this property persistent, XAF initially queries only visible nodes from the database.
- If this property is calculated, XAF loads the next level to allow the Tree List to evaluate whether nodes have children.
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.EF;
using System.Collections.ObjectModel;
namespace SolutionName.Module.BusinessObjects;
[DefaultClassOptions]
public class Category : BaseObject {
public virtual string Name { get; set; }
[HideInUI(HideInUI.ListViewColumn | HideInUI.DetailViewEditor)]
public virtual Guid ParentObjectId { get; set; }
// Add this property if you use Queryable data access mode.
[HideInUI(HideInUI.ListViewColumn | HideInUI.DetailViewEditor)]
[PersistentAlias("[<MyApplication.Module.BusinessObjects.Category>][ParentObjectId = ^.ID and ID != ^.ID]")]
public bool HasChildren => EvaluateAlias<bool>();
}
You can make the HasChildren property persistent (stored in the database):
- Add a getter and setter
- Remove the PersistentAlias attribute
//...
[HideInUI(HideInUI.ListViewColumn | HideInUI.DetailViewEditor)]
public virtual bool HasChildren { get; set; }
If your application uses EF Core Framework, register the Category class in the DBContext.
File: SolutionName.Module\BusinessObjects\SolutionNameDbContext.cs
using DevExpress.ExpressApp.EFCore.Updating;
using DevExpress.Persistent.BaseImpl.EF;
using Microsoft.EntityFrameworkCore;
namespace SolutionName.Module.BusinessObjects;
// ...
public class SolutionNameDbContext : DbContext {
// ...
public SolutionNameDbContext(DbContextOptions<SolutionNameDbContext> options)
: base(options) {
}
// ...
public DbSet<Category> Categories { get; set; }
}
Step-by-Step Scenario
- In your ASP.NET Core Blazor project (SolutionName.Blazor.Server), open Model Editor and navigate to the Category_ListView node.
- In the
EditorTypeproperty, specify the DevExpress.ExpressApp.Blazor.Editors.DxTreeListEditor value. Specify the
KeyFieldName,ParentKeyFieldName, andHasChildrenFieldNameproperties as follows:Property EF Core XPO HasChildrenFieldNameHasChildrenHasChildrenKeyFieldNameIDOidParentKeyFieldNameParentObjectIdParentObjectId- Run the ASP.NET Core Blazor application and click the
Categoryitem in the navigation control. - To create a top-level
Categoryobject, use the New Action. To create a child object, select the parent object in the tree list and click the New action.
XAF displays these objects as a tree in the Category List View:

Set Up One-To-Many Relationship (EF Core)
Add the
ParentObjectandChildrenproperties to theCategoryclass. MakeParentObjectIdnullable to use it as a foreign key.File: SolutionName.Module\BusinessObjects\Category.cs
using DevExpress.Persistent.Base; using DevExpress.Persistent.BaseImpl.EF; using System.Collections.ObjectModel; namespace SolutionName.Module.BusinessObjects; [DefaultClassOptions] public class Category : BaseObject { public virtual string Name { get; set; } [HideInUI(HideInUI.ListViewColumn | HideInUI.DetailViewEditor)] public virtual Guid? ParentObjectId { get; set; } public virtual Category ParentObject { get; set; } [HideInUI(HideInUI.ListViewColumn | HideInUI.DetailViewEditor)] public virtual bool HasChildren { get; set; } public virtual IList<Category> Children { get; set; } = new ObservableCollection<Category>(); }To configure the One-to-Many relationship between objects of the
Categorytype, use theOnModelCreatingmethod of yourDbContextdescendant.File: SolutionName.Module\BusinessObjects\SolutionNameDbContext.cs
using DevExpress.ExpressApp.Design; using DevExpress.ExpressApp.EFCore.DesignTime; using DevExpress.ExpressApp.EFCore.Updating; using DevExpress.ExpressApp.Security; using DevExpress.Persistent.BaseImpl.EF; using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy; using DevExpress.Persistent.BaseImpl.EFCore.AuditTrail; using Microsoft.EntityFrameworkCore; namespace SolutionName.Module.BusinessObjects; // ... public class SolutionNameDbContext : DbContext { // ... public SolutionNameDbContext(DbContextOptions<SolutionNameDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { //... var entity = modelBuilder.Entity<Category>(); entity .HasMany(x => x.Children) .WithOne(x => x.ParentObject) .HasForeignKey(x => x.ParentObjectId); //... } }- In your ASP.NET Core Blazor project (SolutionName.Blazor.Server), open Model Editor and navigate to the Category_Children_ListView node.
- In the
EditorTypeproperty, specify the DevExpress.ExpressApp.Blazor.Editors.DxTreeListEditor value. Specify the
KeyFieldName,ParentKeyFieldName, andHasChildrenFieldNameproperties as follows:Property Value HasChildrenFieldNameHasChildrenKeyFieldNameIDParentKeyFieldNameParentObjectIdThese settings also allow your Tree List editor to handle
HasChildrencollection updates automatically.
Set Up One-To-Many Relationship (XPO)
- Add the
ParentObjectandChildrenproperties to theCategoryclass. - To configure the One-to-Many relationship between objects of the
Categorytype, use the Association attribute. Handle the
CollectionChangedevent of theChildrencollection to update theHasChildrenproperty when child objects are removed.using DevExpress.Persistent.Base; using DevExpress.Persistent.BaseImpl; using DevExpress.Xpo; namespace SolutionName.Module.BusinessObjects; [DefaultClassOptions] public class Category : BaseObject { public Category(Session session) : base(session) { } private string name; public string Name { get => name; set => SetPropertyValue(nameof(Name), ref name, value); } [PersistentAlias("IsNull(ParentObject.Oid, {00000000-0000-0000-0000-000000000000})")] [HideInUI(HideInUI.ListViewColumn | HideInUI.DetailViewEditor)] public Guid ParentObjectId => (Guid)EvaluateAlias(); private Category parentObject; [Association("Parent-Children")] public Category ParentObject { get => parentObject; set { var oldParent = parentObject; SetPropertyValue(nameof(ParentObject), ref parentObject, value); if(value == null && oldParent != null) { oldParent.UpdateHasChildren(); } } } [HideInUI(HideInUI.ListViewColumn | HideInUI.DetailViewEditor)] public bool HasChildren { get => GetPropertyValue<bool>(); set => SetPropertyValue(nameof(HasChildren), value); } [Association("Parent-Children")] public XPCollection<Category> Children => GetCollection<Category>(); protected override void OnLoaded() { base.OnLoaded(); Children.CollectionChanged += Children_CollectionChanged; } private void Children_CollectionChanged(object sender, XPCollectionChangedEventArgs e) { if(e.CollectionChangedType == XPCollectionChangedType.AfterRemove) { UpdateHasChildren(); } } private void UpdateHasChildren() { HasChildren = Children.Any(x => !x.IsDeleted && x.ParentObject?.Oid == Oid); } //... }Create a controller to handle the assignment of the
ParentObjectproperty when new child objects are created or existing child objects are orphaned.File: SolutionName.Blazor.Server\Controllers\CategoryController.cs
using DevExpress.ExpressApp; using DevExpress.ExpressApp.Blazor.Editors; using DevExpress.ExpressApp.Blazor.SystemModule; namespace SolutionName.Blazor.Server.Controllers; public class CategoryController : ObjectViewController<ListView, Category> { private TreeNewFlatObjectController newTreeNodeController; private TreeFlatParentObjectUpdateController parentUpdateController; protected override void OnActivated() { base.OnActivated(); if(View.Editor is not DxTreeListEditor) { return; } newTreeNodeController = Frame.GetController<TreeNewFlatObjectController>(); newTreeNodeController.ProcessNewTreeObject += NewTreeNodeController_ProcessNewTreeObject; parentUpdateController = Frame.GetController<TreeFlatParentObjectUpdateController>(); parentUpdateController.ChildNodeOrphaned += ParentUpdateController_ChildNodeOrphaned; } protected override void OnDeactivated() { base.OnDeactivated(); if(View.Editor is not DxTreeListEditor) { return; } if(newTreeNodeController != null) { newTreeNodeController.ProcessNewTreeObject -= NewTreeNodeController_ProcessNewTreeObject; newTreeNodeController = null; } if(parentUpdateController != null) { parentUpdateController.ChildNodeOrphaned -= ParentUpdateController_ChildNodeOrphaned; parentUpdateController = null; } } private void NewTreeNodeController_ProcessNewTreeObject(object sender, ProcessNewTreeObjectEventArgs e) { var node = (Category)e.NewTreeNode; var parent = (Category)e.ParentNode; node.ParentObject = parent; } private void ParentUpdateController_ChildNodeOrphaned(object sender, ChildNodeOrphanedEventArgs e) { var node = (Category)e.Node; node.ParentObject = null; } }In your ASP.NET Core Blazor project (SolutionName.Blazor.Server), open Model Editor and navigate to the Category_Children_ListView node.
- In the
EditorTypeproperty, specify the DevExpress.ExpressApp.Blazor.Editors.DxTreeListEditor value. Specify the
KeyFieldName,ParentKeyFieldName, andHasChildrenFieldNameproperties as follows:Property Value HasChildrenFieldNameHasChildrenKeyFieldNameOidParentKeyFieldNameParentObjectId