Creating a Generic Tree View Blazor Component

Posted by

This post has been republished via RSS; it originally appeared at: New blog articles in Microsoft Tech Community.

Creating a Generic Tree View Blazor Component.png

I want to take a moment to show off a Blazor component that I made that can display an object recursively in a tree view. The component was made as part of my team’s project, FHIR Watch, a tool for comparing FHIR data from two different data sources: FHIR API Service and Dataverse. As such, it is particularly useful for displaying FHIR data.

 

Prerequisites

  • A Blazor project
  • An object with interesting data and multiple levels for demonstration

 

Why

If you’re doing data comparison like we were or need to view data your app is receiving in a simple format, a tree view is a great way for users to understand complex objects quickly. This tree view is made to be as generic as possible and so can work with a variety of flavors of data.

 

Our Code Behind

For our component to work, we needed to take in an incoming object and convert into an object that our component can understand. To do this, we created a new class which we'll call Branch. Our component will then take as a property a single root Branch called Trunk. Each Branch can own its own Branches.

 

 

 

 

using Microsoft.AspNetCore.Components; using Newtonsoft.Json.Linq; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; namespace FhirWatch.SharedComponents { public partial class Treeview { [Parameter] public Branch Trunk { get; set; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "<Pending>")] private static void SpanToggle(EventArgs e, Branch item) { item.Expanded = !item.Expanded; } } }

 

 

 

 

Because we want to handle complex objects with depth to them, we need our class to not just preserve the value which we will need to display of properties but also keep track of the depth of our recursion and within a layer of the recursion – the iteration if there is more than one property at that layer. In addition, we can add a Boolean to control whether or not the user has expanded a particular node in the tree. Also, because I like to distinguish between properties and arrays, I added a Boolean to tell, if a node that has children, is it an object or an array so we can characterize them in our display.

 

 

 

 

public class Branch { public int Id { get; set; } public int LayerId { get; set; } public string Name { get; set; } public string Value { get; set; } public List<Branch> Branches { get; set; } public bool Expanded { get; set; } = false; public bool IsObj { get; set; } = false;

 

 

 

 

Now that we have our properties, we need to add a constructor that can ingest a JToken. JTokens are the base class for working with JSON objects in Newtonsoft.JSON, a popular library for working with JSON in dotnet. In our constructor, we will recurse through our JToken and create new branches for each property at every layer of the object and order them by layer and ID (viz. the iteration in that layer). When looking at a JToken, we need to decide if it is showing an object, array, or primitive. If it is primitive, we can simply convert the value into something we can display.

 

 

 

 

public Branch(JToken jToken, string name, int id, int layerId = 0) { Id = id; LayerId = layerId; if (jToken == null) return; var parent = jToken.Parent as JProperty; Name = name ?? parent?.Name ?? $"[{id}]"; switch (jToken.Type) { case JTokenType.None: case JTokenType.Null: case JTokenType.Undefined: Value = null; Branches = null; break; case JTokenType.String: case JTokenType.Uri: case JTokenType.Integer: case JTokenType.Float: case JTokenType.Boolean: Value = jToken.ToString(); Branches = null; break; case JTokenType.Date: case JTokenType.TimeSpan: Value = jToken.ToString(); Branches = null; break; case JTokenType.Property: var prop = jToken as JProperty; Name = prop.Name; if (prop.Value is JArray) { Value = null; Branches = prop.Value.Children().Select((c, i) => new Branch(c, null, i, layerId + 1)).ToList(); } else { Value = prop.Value.ToString(); Branches = null; } break; case JTokenType.Object: Value = null; Branches = jToken.Children().Select((c, i) => new Branch(c, null, i, layerId + 1)).ToList(); IsObj = true; break; case JTokenType.Array: Value = null; Branches = jToken.Children().Select((c, i) => new Branch(c, null, i, layerId + 1)).ToList(); break; case JTokenType.Constructor: case JTokenType.Comment: case JTokenType.Raw: case JTokenType.Guid: case JTokenType.Bytes: default: throw new NotImplementedException(); } }

 

 

 

 

If it is an object or an array, our recursion continues. In our FHIR Watch project, this constructor is for our Dataverse data which contains FHIR-adjacent data. However, our data from the Azure FHIR service is actual FHIR data and as such we were able to use the library, Firely, to work with that data. When I wrote this component though, I wanted it to work for more than just FHIR data so for the non-JToken constructor, I had it just consume an object.

 

 

 

 

public Branch(object obj, string name, int id, int layerId = 0) { Id = id; LayerId = layerId; // if the object is a primitive or otherwise human readable, set value if (obj == null) { Value = null; Branches = null; return; } else if (IsPrimitive(obj) || OverridesToString(obj)) { Value = obj.ToString(); Branches = null; } else if (IsDate(obj)) { Value = DateTime.TryParse(obj as string, out DateTime dateTime) ? dateTime.ToLongDateString() : "!error parsing date!"; Branches = null; } else { Value = null; // if list of things, convert things to branches if (IsList(obj)) { var list = ((IEnumerable)obj).Cast<object>(); //var list = obj as List<object>; Branches = list.Where(o => o != null).Select((o, i) => new Branch(o, $"[{i}]", i, layerId++)).Where(b => !string.IsNullOrEmpty(b.Name)).ToList(); } else { // otherwise convert properties to branches var props = obj.GetType().GetProperties().ToDictionary(pi => pi.Name, pi => pi.GetValue(obj)); Branches = props.Where(kvp => kvp.Value != null).Select((kvp, i) => new Branch(kvp.Value, kvp.Key, i, layerId++)).Where(b => !string.IsNullOrEmpty(b.Name)).ToList(); IsObj = true; } } Name = name; }

 

 

 

 

For this constructor, checking for primitive (primitive-like data where it is simple to translate it to a human readable string e.g., a datetime) becomes a bit trickier. One handy method for accomplishing this was to check if the object had overwritten the ToString method - indicating that there was a strong opinion from the author of that type on how it should be displayed. This tricky allowed me to display FHIR Dates which are a different class than your standard datetime. To improve readability, I moved many of these conditionals into private helpers:

 

 

 

private static bool IsDate(object obj) { return DateTime.TryParse(obj as string, out _) || obj.GetType() == typeof(DateTimeOffset); } private static bool OverridesToString(object obj) { MethodInfo methodInfo = obj.GetType().GetMethod("ToString", new[] { typeof(string) }); if (methodInfo == null) return false; return methodInfo.DeclaringType != methodInfo.GetBaseDefinition().DeclaringType; } private static bool IsList(object obj) { try { return obj.GetType().GetGenericTypeDefinition() == typeof(List<>); } catch { return false; } } private static bool IsPrimitive(object obj) { if (obj == null) return true; var type = obj.GetType(); return obj is string || obj is decimal || type.IsEnum || obj.GetType().IsPrimitive; }

 

 

 

 

Our Blazor HTML

Now that we have a collection of branches, we can now display a tree. Our HTML will be pretty simple, we will take our trunk – the base branch – and display all of its branches.

 

 

 

 

@using Microsoft.AspNetCore.Components.Web <div class="container"> <ul class="@(Trunk.LayerId == 0 ? "parentUl": "")"> <li> @if (Trunk.Branches != null && Trunk.Branches.Any()) { <span @onclick="@(e => SpanToggle(e, Trunk))" class="caret @(Trunk.Expanded ? "caret-down" : "")">@Trunk.Name: @(Trunk.Value ?? (Trunk.IsObj ? "{...}" : "[...]")) </span> <ul class="@(Trunk.Expanded ? "active" : "nested")"> @foreach (var branch in Trunk.Branches) { <Treeview Trunk="branch" /> } </ul> } else if (Trunk.Branches != null) { <span>@Trunk.Name: @(Trunk.IsObj ? "{...}" : "[...]")</span> } else { <span>@Trunk.Name: @(Trunk.Value ?? string.Empty) </span> } </li> </ul> </div>

 

 

 

 

For each branch that has branches of its own, we will call our Treeview component to display its branches, taking full advantage of Blazor’s ability to handle recursive component tags.

 

 

 

 

@foreach (var branch in Trunk.Branches) { <Treeview Trunk="branch" /> }

 

 

 

 

 

Our Scoped CSS

Lastly, to style our tree view, we can borrow directly from WC3’s tutorial on making tree views with raw Javascript. I recommend checking it out, it was influential for this component.

 

Conclusion

This was a great exercise in recursion and a useful component to keep in your back pocket. My two big takeaways from writing this component were:

  • Recursive tagging Blazor is both possible and cool
  • Keeping track of recursive depth is a must

I hope you enjoyed this write up. If you like the content I put out or want to be part of a community of healthcare developers sharing knowledge and resources, check out our HLS Developer discord at https://aka.ms/HLS-Discord. We have links to all our content there and a bunch of channels to communicate with us and likeminded tech and healthcare people. Hope to see you there.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.