Thursday, 31 December 2020

Simple property grid in Blazor

This artile will present a simple property grid in Blazor I have made. The component relies on standard stuff like Bootstrap, jQuery, Twitter Bootstrap and Font Awesome. But the repo url shown here links to the Github repo of mine which can be easily forked if you want to add features (such as editing capabilities). The component already supports nested levels, so if the object you inspect has a hierarchical structure, this is shown in this Blazor component. Having a component to inspect objects in Blazor is great as Blazor lacks inspect tools (since the app is compiled into a web assembly, we cannot easily inspect state of objects in the app other than the DOM and Javascript objects. With this component we can get basic inspection support to inspect state of the object in the app you desire to inspect). The Github repo contains also a bundled application which uses the component and shows a sample use-case (also shown in Gif video below). I have tested the component with three levels of depth for a sample object (included in the repo). The component is available here on my Github repo:
 
 git clone https://github.com/toreaurstadboss/BlazorPropertyGrid/tree/main/BlazorPropertyGrid
 
https://github.com/toreaurstadboss/BlazorPropertyGrid/tree/main/BlazorPropertyGrid
The component consists of two components where one of them is used in a recursive manner to support nested object structure. The top level component got this code-behind class.
PropertyGridComponentBase.cs
using System.Collections.Generic; using System.Reflection; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.JSInterop; namespace BlazorPropertyGridComponents.Components { public class PropertyGridComponentBase : ComponentBase { [Inject] public IJSRuntime JsRuntime { get; set; } [Parameter] public object DataContext { get; set; } public Dictionary<string, PropertyInfoAtLevelNodeComponent> Props { get; set; } public PropertyGridComponentBase() { Props = new Dictionary<string, PropertyInfoAtLevelNodeComponent>(); } protected override void OnParametersSet() { Props.Clear(); if (DataContext == null) return; Props["ROOT"] = MapPropertiesOfDataContext(string.Empty, DataContext, null); StateHasChanged(); } private bool IsNestedProperty(PropertyInfo pi) => pi.PropertyType.IsClass && pi.PropertyType.Namespace != "System"; private PropertyInfoAtLevelNodeComponent MapPropertiesOfDataContext(string propertyPath, object parentObject, PropertyInfo currentProp) { if (parentObject == null) return null; var publicProperties = parentObject.GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance); var propertyNode = new PropertyInfoAtLevelNodeComponent { PropertyName = currentProp?.Name ?? "ROOT", PropertyValue = parentObject, PropertyType = parentObject.GetType(), FullPropertyPath = TrimFullPropertyPath($"{propertyPath}.{currentProp?.Name}") ?? "ROOT", IsClass = parentObject.GetType().IsClass && parentObject.GetType().Namespace != "System" }; foreach (var p in publicProperties) { var propertyValue = p.GetValue(parentObject, null); if (!IsNestedProperty(p)) { propertyNode.SubProperties.Add(p.Name, new PropertyInfoAtLevelNodeComponent { IsClass = false, FullPropertyPath = TrimFullPropertyPath($"{propertyPath}.{p.Name}"), PropertyName = p.Name, PropertyValue = propertyValue, PropertyType = p.PropertyType //note - SubProperties are default empty if not nested property of course. } ); } else { //we need to add the sub property but recurse also call to fetch the nested properties propertyNode.SubProperties.Add(p.Name, new PropertyInfoAtLevelNodeComponent { IsClass = true, FullPropertyPath = propertyPath + p.Name, PropertyName = p.Name, PropertyValue = MapPropertiesOfDataContext(TrimFullPropertyPath($"{propertyPath}.{p.Name}"), propertyValue, p), PropertyType = p.PropertyType //note - SubProperties are default empty if not nested property of course. } ); } } return propertyNode; } protected void toggleExpandButton(MouseEventArgs e, string buttonId) { JsRuntime.InvokeVoidAsync("toggleExpandButton", buttonId); } private string TrimFullPropertyPath(string fullpropertypath) { if (string.IsNullOrEmpty(fullpropertypath)) return fullpropertypath; return fullpropertypath.TrimStart('.').TrimEnd('.'); } } }
And its razor file looks like this:
PropertyGridComponentBase.razor
@inherits PropertyGridComponentBase @using BlazorPropertyGridComponents.Components <table class="table table-striped col-md-4 col-lg-3 col-sm-6"> <thead> <tr> <th scope="col">Property</th> <th scope="col">Value</th> </tr> </thead> <tbody> @foreach (KeyValuePair<string, PropertyInfoAtLevelNodeComponent> prop in Props) { @if (!prop.Value.IsClass) { @* <tr> <td>@prop.Key</td> <td>@prop.Value</td> </tr>*@ } else { var currentNestedDiv = "currentDiv_" + prop.Key; var currentProp = prop.Value.PropertyValue; //must be a nested class property <tr> <td colspan="2"> <button type="button" id="@prop.Key" class="btn btn-info fas fa-minus" @onclick="(e) => toggleExpandButton(e,prop.Key)" data-toggle="collapse" data-target="#@currentNestedDiv"> </button> <div id="@currentNestedDiv" class="collapse show"> <PropertyRowComponent Depth="1" PropertyInfoAtLevel="@prop.Value" /> </div> </td> </tr> } } </tbody> </table> @code { }
We also have this helper class to model each property in the nested structure:
PropertyInfoAtLevelNodeComponent.cs
using System; using System.Collections.Generic; namespace BlazorPropertyGridComponents.Components { /// <summary> /// Node class for hierarchical structure of property info for an object of given object graph structure. /// </summary> public class PropertyInfoAtLevelNodeComponent { public PropertyInfoAtLevelNodeComponent() { SubProperties = new Dictionary<string, PropertyInfoAtLevelNodeComponent>(); } public string PropertyName { get; set; } public object PropertyValue { get; set; } public Type PropertyType { get; set; } public Dictionary<string, PropertyInfoAtLevelNodeComponent> SubProperties { get; private set; } public string FullPropertyPath { get; set; } public bool IsClass { get; set; } } }
Our lower component used by the top component code-behind looks like this:
PropertyRowComponentBase.cs
using System.Collections.Generic; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.JSInterop; namespace BlazorPropertyGridComponents.Components { public class PropertyRowComponentBase : ComponentBase { public PropertyRowComponentBase() { DisplayedFullPropertyPaths = new List<string>(); } [Parameter] public PropertyInfoAtLevelNodeComponent PropertyInfoAtLevel { get; set; } [Parameter] public int Depth { get; set; } [Parameter] public List<string> DisplayedFullPropertyPaths { get; set; } [Inject] protected IJSRuntime JsRunTime { get; set; } protected void toggleExpandButton(MouseEventArgs e, string buttonId) { JsRunTime.InvokeVoidAsync("toggleExpandButton", buttonId); } } }
The razor file looks like this:
PropertyRowComponent.razor
@using BlazorPropertyGridComponents.Components @inherits PropertyRowComponentBase @foreach (var item in PropertyInfoAtLevel.SubProperties.Keys) { var propertyInfoAtLevel = PropertyInfoAtLevel.SubProperties[item]; if (propertyInfoAtLevel != null) { @* if (DisplayedFullPropertyPaths.Contains(propertyInfoAtLevel.FullPropertyPath)){ continue; //the property is already displayed. }*@ DisplayedFullPropertyPaths.Add(propertyInfoAtLevel.FullPropertyPath); @* <span class="text-white bg-dark">@propertyInfoAtLevel.FullPropertyPath</span>*@ @* <em> @propertyInfoAtLevel </em>*@ } if (!propertyInfoAtLevel.PropertyType.IsClass || propertyInfoAtLevel.PropertyType.Namespace.StartsWith("System")) { <tr> <td> <span title="@propertyInfoAtLevel.FullPropertyPath" class="font-weight-bold">@propertyInfoAtLevel.PropertyName</span> </td> <td> <span>@propertyInfoAtLevel.PropertyValue</span> </td> </tr> } else if (propertyInfoAtLevel.PropertyValue != null && propertyInfoAtLevel.PropertyValue is PropertyInfoAtLevelNodeComponent) { var nestedLevel = (PropertyInfoAtLevelNodeComponent)propertyInfoAtLevel.PropertyValue; var collapseOrNotCssClass = Depth == 0 ? "collapse show" : "collapse"; var curDepth = Depth + 1; collapseOrNotCssClass += " depth" + Depth; var currentNestedDiv = "collapsingdiv_" + propertyInfoAtLevel.PropertyName; //must be a nested class property <tr> <td colspan="2"> <span>@propertyInfoAtLevel.PropertyName</span> <button id="@propertyInfoAtLevel.FullPropertyPath" type="button" @onclick="(e) => toggleExpandButton(e,propertyInfoAtLevel.FullPropertyPath)" class="fas btn btn-info fa-plus" data-toggle="collapse" data-target="#@currentNestedDiv"></button> <div id="@currentNestedDiv" class="@collapseOrNotCssClass"> <PropertyRowComponent PropertyInfoAtLevel="@nestedLevel" Depth="@curDepth" /> </div> </td> </tr> } } @code { }

Monday, 28 December 2020

More fun with Blazor - creating a folder viewer in 10 minutes

 
   git clone https://github.com/toreaurstadboss/BlazorLiveReloadSample.git 
 
Blazor is very easy to use! I spent 10 minutes to create a simple folder viewer here now.
First off, the Blazor razor component looks like this:
FilewView.razor
@inject IJSRuntime jsRuntime; <h3>FileView</h3> @foreach (var folder in folders) { var depthOfFolder = folder.Split('\\').Count(); <p style="cursor:pointer" @onclick="() => openFolder(folder)"> @for (int i = 0; i < depthOfFolder; i++) { <span style="margin-left:10px"></span> } <i style="color:orange;cursor:pointer" class="fa fa-folder" /> @folder </p> } @code { List<string> folders = Directory.GetDirectories("/").ToList(); private void openFolder(string folder) { jsRuntime.InvokeVoidAsync("log", folder); string[] subfolders = Directory.GetDirectories(folder); Console.WriteLine(folder); int folderIndex = folders.IndexOf(folder); folders.InsertRange(folderIndex, subfolders); } }
We add to the solution also Font-awesome via right click solution explorer and choose Add => Client-Side Library. Search for 'font-awesome' Choose Font Awesome and add all files to be added to the lib/font-awesome folder of wwwroot. Then at the bottom of _Host.cshtml we add:
 
_Host.cshtml
<link rel="stylesheet" href="~/lib/font-awesome/css/all.css" />
Now we have access to the Font Awesome icons.

Live reloads for Blazor - and a simple clock component

This article will test out Blazor. I had some difficulties with getting live reload to work. I got it working in Visual Studio 2019 for the Blazor Asp.Net Core project template. We will also create a very simple component (a clock) that calls Javascript function from C#. You can clone the simple app of mine from Github like this:
 
 
 git clone https://github.com/toreaurstadboss/BlazorLiveReloadSample.git
 
 
First off, we add the following into _host.cshtml :
 
_Host.cshtml
<script src="js/script.js"></script> <script src="_framework/blazor.server.js"></script> <script> Blazor.defaultReconnectionHandler._reconnectCallback = function (d) { document.location.reload(); } </script>
The Blazor.defaultReconnectionHandler._reconnectCallback is set to reload the document location This makes the page reload when you edit the razor files of the Blazor app. You will see this as a temporarily recompile step - give it some 5 seconds in a simple app.
Let's for fun add a clock component also. Add to the Shared folder the file Clock.razor.
 
Clock.razor
@inject IJSRuntime JsRunTime @implements IDisposable

The time is now:

00:00:00
@code { ElementReference timeDiv; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JsRunTime.InvokeVoidAsync("startTime", timeDiv); } } public void Dispose() { JsRunTime.InvokeVoidAsync("stopTime"); } }
And we have also the script.js file in wwwroot to add some Javascript (Blazor razor files dont like Js in the component itself, just make sure to add the Js somewhere in wwwroot instead which loads up the necessary Js). As you can see we inject with the @inject in the razor Blazor file (rhymes a bit) the IJsRunTime. This allows us to call client-side code from the C# code. We start off the clock with a setTimeout and stop the clock with a clearTimeout.
 
Clock.razor
var clock; function startTime(element) { let timeString = new Date().toLocaleTimeString('nb-No', { hour: 'numeric', hour12: false, minute: 'numeric', second: 'numeric' }); element.innerHTML = timeString; clock = setTimeout(startTime.bind(null, element), 1000); } function stopTime() { clearTimeout(clock); }