Telerik blogs
blazort_870x220

The TreeView in Telerik UI for Blazor lets you control when and how each set of nodes gets loaded. That not only lets you control how the nodes are loaded but can also improve your page’s performance.

The TreeView in Telerik UI for Blazor allows you to decide what happens when a node is expanded — including how the nodes that are added to your tree are retrieved.

The Telerik TreeView component makes it easy for you to pull data out of a database and load it into the TreeView. But what if your data isn’t in a database or you want to “massage” it before displaying it or the process of retrieving your nodes is, in some way, “complicated”? The TreeView lets you retrieve the data for your nodes by providing a code-based way to load related data.

This strategy of retrieving nodes when needed can also help you improve performance. If, for example, you know that most of the time, most of the nodes on your treeview won’t be expanded, using this technique allows you to reduce the data required for the initial display of the treeview to just the top-level objects. Alternatively, if you were considering a treeview that’s more than two levels deep — well, the more levels you add to a treeview, the more data you have to retrieve, and the longer it takes to get to that initial display of your treeview. Retrieving only the nodes you need when you need them helps in both of these scenarios.

Version caveats: I tested this code in Visual Studio 2019 (16.3.1) which comes with ASP.NET Core 3.0 (which, in turn, includes the first production release of Blazor). I also used Telerik.UI.for.Blazor NuGet package, version 2.1.0.

Setting Up Your Objects

The first step in the process is to define the object that you’re going to use for the top-level nodes of your treeview. You can retrieve those objects from your database but, to support the Telerik TreeView, each object needs some additional properties that won’t exist in your tables:

  • HasChildren: A boolean property that’s set to true to indicate that a node can be expanded (i.e. has “child” nodes)
  • Expanded: A boolean property that indicates whether the node is being expanded by the user or contracted

In addition, your top-level objects will also need some collection property that holds the objects to be display as your second-level nodes in the treeview (this property may already exist in your data’s object model). Effectively then, you need a Data Transfer Object (a DTO) that combines UI-related data with data from your database.

As an example, this CustomerDto object has customer-related data from the database (CustId and FullName), a collection property holding all the Address objects associated with the Customer, plus the two UI-required properties (HasChildren and Expanded):

public class CustomerDto
{
   public int CustId { get; set; }
   public string FullName { get; set; }
   public IEnumerable<Address> Addresses { get; set; }
   public bool HasChildren { get; set; }
   public bool Expanded { get; set; }
}

I covered a LINQ-and-Entity-Framework approach to creating an object like this in an earlier post.

Defining the TreeView

With this in place, you’re ready to add a Telerik TreeView to your component. Markup like the following would do the trick:

<TelerikTreeView Data="@custs" OnExpand="@GetAddresses">
    <TreeViewBindings>
    </TreeViewBindings>
</TelerikTreeView>

In the TreeView, you must set the Data attribute to a field or property in your component’s code section that holds the collection of top-level objects you want to display (in my case, that’s a field called custs that holds my CustomerDto objects). To use code to retrieve and display the child/second-level objects, you must set the TreeView’s OnExpand attribute to the name of some method in your component’s code section (I’ve called my method GetAddresses).

In the TreeViewBindings, you’ll need at least two TreeViewBinding elements. The first TreeViewBinding references the top-level nodes (CustomerDto, in my case). The TextField must be set to some property on that object (I’ve used FullName) and the ItemsField attribute must be set to the property that holds the second-level/child objects (in this case, my CustomerDto object’s Addresses collection). If you’d like to do more than display a single property in a TreeView node, see my post on creating TreeView templates. Here’s that first TreeViewBinding:

<TelerikTreeView Data="@custs" OnExpand="@GetAddresses">
    <TreeViewBindings>
        <TreeViewBinding TextField="FullName" ItemsField="Addresses" />
    </TreeViewBindings>
</TelerikTreeView>

The second TreeViewBinding describes those second-level/child set of nodes. That will be, in my case, the individual Address objects for the CustomerDto’s Addresses property. For this example, I’m going to display the Address object’s City property in each of those second-level/child nodes. Here’s the complete markup for the TreeView:

<TelerikTreeView Data="@custs" OnExpand="@GetAddresses">
    <TreeViewBindings>
        <TreeViewBinding TextField="FullName" ItemsField="Addresses" />
        <TreeViewBinding Level="1" TextField="City" />
    </TreeViewBindings>
</TelerikTreeView>

If I was going to add a third level (i.e. children of the Address object), I could add an ItemsField attribute to the second TreeViewBinding, along with a third TreeViewBinding element to describe that third level.

Initializing the TreeView

In my component’s code section I have two things to do. The first is to load the field that I referred to in my TreeView’s ItemsField attribute that holds the top-level nodes. First, I need to define the field that holds my collection of CustomerDto objects:

@code {
    private IEnumerable<CustomerDto> custs;

Then I need to load that field with the objects that make up my top-level nodes. I want to do that early in my component’s life cycle so I put that code in the component’s OnInitializedAsync method. Here’s what that looked like:

    protected async override Task OnInitializedAsync()
    {
        custs = await CustomerRepository.GetAllAsync();
        await base.OnInitializedAsync();
    }

Loading the Children

Now I need to write the code that loads the second-level/child nodes (the Address objects associated with each customer). I have to put that code in the method I referred to in the TreeView’s onExpand attribute (I called that method GetAddresses). That method will automatically be passed a TreeViewExpandEventArgs object, which I need to catch to use in my method. Here’s what that method’s declaration looks like:

private async Task GetAddresses(TreeViewExpandEventArgs args)
{

Fundamentally, what I’m going to do in this method is use the TreeViewExpandEventArgs’ properties to determine if the node is being expanded (if the node is being collapsed, I don’t have to do anything). Then, if the node is being expanded, I’ll retrieve the child objects (the addresses for the customer, in this case) and stuff them into the property I referenced in the TreeViewBinding element’s ItemsField property (in this case, that would be the CustomerDto object’s Addresses property). The TreeView will take care of displaying them correctly.

There are two wrinkles I need to address in this code, though. First, if that property is already loaded, I don’t need to retrieve those object again (unless, of course, I figure that the underlying data is so volatile that I always need to retrieve the most up-to-date data). And, as I said earlier, if I have multiple levels that can be expanded, I also need to check what type of node I’m dealing with. If, for example, a user could also expand the Address node, I’ll need to include code to load its children.

Taking all of that into account, my GetAddresses method first uses the Expanded property on the TreeViewExpandEventArgs element to see if the node is being expanded or collapsed (if Expanded is true then the node is being expanded). I then use the TreeViewExpandEventArgs’ Item property (which holds the node that’s being expanded) to determine if I’m working with a CustomerDto object. To support using this method for other types of objects, I’ll set this up as a switch block inside an if block:

if (args.Expanded)
{
   switch (args.Item.GetType().Name)
  {
     case "CustomerDto":

Now that I know both that the node is expanded and that I am dealing with a CustomerDto object, I’ll cast that node into a CustomerDto variable so that I can get at the CustomerDto class’s properties:

CustomerDto cust = args.Item as CustomerDto;

The next step is to determine if I’ve already loaded the Addresses property (that could happen if the user is expanding a node that was expanded earlier). If so, I don’t have to do anything and I can just exit my method:

if (cust.Addresses != null)
{
   return;
}

Now that I’ve handled that “special” case, I can retrieve the Address objects for the Customer and stuff them into the Addresses property for the CustomerDto that’s being expanded. I also need to call Blazor’s StateHasChanged method to cause Blazor to display the change in the TreeView’s UI. If I’ve got the right classes in place, that code can be as simple as this (I’ve also included the break statement that the switch structure requires):

cust.Addresses = await AddressRepository.GetAddressesForCustomer(cust.CustId);
StateHasChanged();
break;

I’ve probably made this sound more complicated than it is — altogether, my GetAddresses method just has nine lines of actual code. Here it is in full:

private async Task GetAddresses(TreeViewExpandEventArgs args)
{
   if (args.Expanded)
   {
      switch (args.Item.GetType().Name)
      {
         case "CustomerDto":
                    CustomerDto cust = args.Item as CustomerDto;
                    if (cust.Addresses != null)
                    {
                        return;
                    }
                    cust.Addresses = await AddressRepositoryAsync.GetAddressesForCustomer(cust.CustId);
                    break;
      }         
      StateHasChanged();   
   }
}

I can, in fact, continue to customize what the Address objects attached to my TreeView nodes look like. There’s nothing stopping me from creating an Address DTO object with a Customer property that I could load with the related Customer object — that might come in handy as the user interacts with the Address nodes. The additional line of code before the break statement in my case statement would look like this:

cust.Addresses.ToList().ForEach(c => c.Customer = cust);

And I can continue to extend this model. To support, for example, another level of nodes underneath the Address nodes, I would only have to extend the switch block with another case block (another six lines of code). That’s not bad for complete control over how your treeview adds nodes.

Try it Today

To learn more about Telerik UI for Blazor components and what they can do, check out the Blazor demo page or download a trial to start developing right away.

Start Trial


Peter Vogel
About the Author

Peter Vogel

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter also writes courses and teaches for Learning Tree International.

Related Posts

Comments

Comments are disabled in preview mode.