microsoft blazor

Microsoft Blazor – Custom controls for dynamic content

G’day! Welcome to the continuation of my previous blog: 

Today I would like to demonstrate how to create a True dynamic page that can generate and bind controls that the page is unaware of. This is an important feature because, as I described in my previous blog post, the dynamic content is generated using a [switch] statement where all available controls should be added.

You may notice that sometimes I use “controls” and sometimes “components” in my blog posts. Please do not be confused – the terms are interchangeable and they are absolutely the same things. All it means is UI control and both terms are used by the developer community.

Requirements #2: a dynamic generation with custom controls

  • Add support of custom controls to dynamic UI generation
  • Custom controls can be located in a separate assembly for dynamic assembly loading
  • Custom controls should accept two mandatory parameters: [ControlDetails] Control and [Dictionary<string, string>] Values

Implementation- Razor

I will take my previous solution to start with, I copied all the code to a new folder and renamed the solution file, so the final resulting code is stored separately from code from the previous blog (story #1). Again, you can download code from my GitHub page.

https://github.com/euklad/BlogCode/tree/main/DemoDynamicContent-story1

Let’s start with changes to the [Counter.razor] file, we will need to add a case where [Type] of control is unknown and generate this control:

 default:
            var customComponent = GetCustomComponent(control.Type);
            RenderFragment renderFragment = (builder) =>
            {
                builder.OpenComponent(0, customComponent);
                builder.AddAttribute(0, "Control", control);
                builder.AddAttribute(0, "Values", Values);
                builder.CloseComponent();
            };
            <div>
                @renderFragment
            </div>
            break;

This code uses the [RenderTreeBuilder] class to do the custom rendering. We are expected to supply the component type – not the text name of the component but a real .NET type, and then we supply as many component parameters as we want. Because user story #2 specifies 2 mandatory parameters we supply only them.

Now we will need to implement a new method: [GetCustomComponent] that should find the .NET type of the rendered control (component) by name somehow. Of course, we will use dependency injection for that, but before coding it we need to think about the possibility to store custom controls in a separate library.

If we store the controls in a separate library we will probably need to implement the type-resolution logic in the same library (to have access to control’s .NET types), and if we do it in the most elegant way we will use an interface for that (let’s name it [IComponentTypeResolver]), putting the type-resolution logic to a service that implements this interface. So [IComponentTypeResolver] interface should be visible to the type-resolution service.

At the same time [IComponentTypeResolver] should be visible from our dynamic page to be able to consume it, and when we want to consume an interface from two different assemblies that do not have explicit dependencies – we need to create a shared assembly and put the interface there.

Implementation- Libraries

So let’s create a Razor component library first:

razor

By default, it will create the library using .NET Standard 2.0, so change it to version 2.1:

I believe that Microsoft uses .NET Standard instead of .NET Core for purpose because Blazor WebAssembly can be built only on .NET Standard and if you want to reuse your controls in WebAssembly in the future it is better to use .NET Standard framework.

Now we need to create a shared assembly and it should be usable from the .NET Standard library that we just created:

class .NET

Don’t forget to change the framework from .NET Standard 2.0 to version 2.1 and add project references from the main application and from the Razor library to the Shared library.

Now we can implement the interface [IComponentTypeResolver], let’s add new items to the Shared library:

using System;
namespace DemoShared
{
    public interface IComponentTypeResolver
    {
        Type GetComponentTypeByName(string name);
    }
}

Now we can use this interface from the dynamic razor page to find control type by name, and we need to inject IComponentTypeResolver at the top of the file:

...
@inject DemoShared.IComponentTypeResolver _componentResolverService
...
    private Type GetCustomComponent(string name)
    {
        return _componentResolverService.GetComponentTypeByName(name);
    }
...

Thus, the resulting code of [Counter.razor] page will look like:

@page "/counter"
@inject ControlService _controlService
@inject DemoShared.IComponentTypeResolver _componentResolverService
@foreach (var control in ControlList)
{
    @if (control.IsRequired)
    {
        <div>@(control.Label)*</div>
    }
    else
    {
        <div>@control.Label</div>
    }
    @switch (control.Type)
    {
        case "TextEdit":
            <input @bind-value="@Values[control.Label]" required="@control.IsRequired" />
            break;
        case "DateEdit":
            <input type="date" value="@Values[control.Label]" @onchange="@(a => ValueChanged(a, control.Label))" required="@control.IsRequired" />
            break;
        default:
            var customComponent = GetCustomComponent(control.Type);
            RenderFragment renderFragment = (builder) =>
            {
                builder.OpenComponent(0, customComponent);
                builder.AddAttribute(0, "Control", control);
                builder.AddAttribute(0, "Values", Values);
                builder.CloseComponent();
            };
            <div>
                @renderFragment
            </div>
            break;
    }
}
<br />
<button @onclick="OnClick">Submit</button>
@code
{
    private List<ControlDetails> ControlList;
    private Dictionary<string, string> Values;
    protected override async Task OnInitializedAsync()
    {
        ControlList = _controlService.GetControls();
        Values = ControlList.ToDictionary(c => c.Label, c => "");
    }
    void ValueChanged(ChangeEventArgs a, string label)
    {
        Values[label] = a.Value.ToString();
    }
    string GetValue(string label)
    {
        return Values[label];
    }
    private void OnClick(MouseEventArgs e)
    {
        // send your Values
    }
    private Type GetCustomComponent(string name)
    {
        return _componentResolverService.GetComponentTypeByName(name);
    }
}

Now the solution can be compiled and run, but it will throw an exception complaining that [_componentResolverService] cannot be resolved, because it is not registered in dependency injection. We will register the type-resolution service at the final step.

Implementation – Custom controls

Now let’s create a custom control, but before doing that we will need to move [ControlDetails.cs] to the Shared library because this class should be accessible from the Razor library too.

The control code will look like:

@namespace DemoRazorClassLibrary
<div>
    This Blazor component is defined in the <strong>DemoRazorClassLibrary</strong> package.
    <input @bind-value="@Values[Control.Label]" required="@Control.IsRequired" />
</div>
@code 
{
    [Parameter]
    public DemoDynamicContent.ControlDetails Control { get; set; }
    [Parameter]
    public Dictionary<string, string> Values { get; set; }
}

I used [@namespace] to explicitly specify the full name of the control type – now it will be [DemoRazorClassLibrary.Component1] independently in which folder you will move it, and now we can create a [ComponentResolverService] class that will register the created control type in a Dictionary to be able to quickly find its type by name whenever the Blazor engine wants to re-render the page.

The control input parameters marked by [Parameter] attributes and their names are the same names that we supplied in the dynamic page rendering code.

The last bit is the resolver, it will look like:

using DemoShared;
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoRazorClassLibrary
{
    public class ComponentResolverService : IComponentTypeResolver
    {
        private readonly Dictionary<string, Type> _types = new Dictionary<string, Type>();
        public ComponentResolverService()
        {
            _types["Component1"] = typeof(DemoRazorClassLibrary.Component1);
        }
        public Type GetComponentTypeByName(string name)
        {
            return _types[name];
        }
    }
}

Now if we want to register another custom control, we just need to add a new control Razor file and register its type in the [ComponentResolverService] constructor.

Implementation – Running

If we run our solution right now it will not work because we forgot to register [ComponentResolverService] in Dependency Injection. We need to open [Startup.cs] and add the registration line of code, so the [ConfigureServices] method will look like:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor();
            services.AddSingleton<WeatherForecastService>();
            // added line for ControlService
            services.AddSingleton<ControlService>();
            // added line for Type Resolution Service
            services.AddSingleton<DemoShared.IComponentTypeResolver, DemoRazorClassLibrary.ComponentResolverService>();
        }

but this is not enough! We also need to add a project reference from the main project to the Razor library – though we tried to avoid making this reference.

However, Dependency Injection registration of [ComponentResolverService] can be done by loading assemblies at run time to [AppDomain] finding the required type using reflection and registering it. We don’t do that now only for simplification.

Here at Pro Coders we use reflection a lot and maybe in the next blog posts I will show you how to load components dynamically from an assembly that is not referenced – it is a well-known plug-in practice.

We modify the main project [ControlService] stub class to use created custom control [Component1]:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoDynamicContent
{
    public class ControlService
    {
        public List<ControlDetails> GetControls()
        {
            var result = new List<ControlDetails>();
            result.Add(new ControlDetails { Type = "TextEdit", Label = "First Name", IsRequired = true });
            result.Add(new ControlDetails { Type = "TextEdit", Label = "Last Name", IsRequired = true });
            result.Add(new ControlDetails { Type = "DateEdit", Label = "Birth Date", IsRequired = false });
            // add custom control
            result.Add(new ControlDetails { Type = "Component1", Label = "Custom1", IsRequired = false });
            return result;
        }
    }
}

All done! Now let’s run and see the results:

dynamic content

After filling in controls I clicked the [Submit] button, let’s see our [Values] Dictionary in debug:

visual studio

As you can see all the entered values are stored in the Dictionary and we can save it to a database if needed.

User Story #2 was completed.

Summary

This article demonstrated a way of dynamic UI generation when you don’t know all types of controls upfront. This is a well-known challenge for extendable content management systems or dynamic forms frameworks when embedding of new controls to page generation logic is restricted.

Thanks to Microsoft Blazor developers who provided an elegant way for a custom rendering with [RenderTreeBuilder] class.

See you next time and thank you for reading.