Click here to Skip to main content
14,662,527 members
Articles » Web Development » ASP.NET » Howto
Article
Posted 15 Oct 2020

Stats

5.1K views
8 bookmarked

Microsoft Blazor - Custom Controls for Dynamic Content

Rate this:
5.00 (6 votes)
Please Sign up or sign in to vote.
5.00 (6 votes)
17 Oct 2020CPOL
Demonstration of how to create an externally extendable dynamic page, which will support all controls that we can add later in a separate assembly without the recompilation of the dynamic page
This article demonstrates 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.

Microsoft Blazor - Dynamic Content

Introduction

Good day! Welcome to the continuation of my previous blog "Microsoft Blazor - Dynamic content". In this post, 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.

User Story #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.

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:

Image 1

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

Image 2

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 the .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:

Image 3

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 the 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 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 class="my-component">
    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, we can try moving the Counter.Razor page to a new assembly, and then it will not be any dependencies between the dynamic page and the custom control.

Alternatively, 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:

Image 4

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

Image 5

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.

History

  • 16th October, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

euklad
Software Developer (Senior) Pro Coders
Australia Australia
Programming enthusiast and the best practices follower

Comments and Discussions

 
PraiseNice job, well done Pin
Member 202563020-Oct-20 12:14
MemberMember 202563020-Oct-20 12:14 
QuestionYou have made a good job ! Pin
FriedhelmEichin19-Oct-20 3:25
MemberFriedhelmEichin19-Oct-20 3:25 
AnswerRe: You have made a good job ! Pin
euklad19-Oct-20 15:07
professionaleuklad19-Oct-20 15:07 
PraiseGreat! Pin
Ashok Kumar RV16-Oct-20 22:46
MemberAshok Kumar RV16-Oct-20 22:46 
PraiseGreat article, looking forward to the next one Pin
EvaMor Developer16-Oct-20 2:27
professionalEvaMor Developer16-Oct-20 2:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.