Telerik blogs
DotNetT2 Dark_1200x303

You won’t always know which components you need to render ahead of time. Here’s how to render components dynamically in response to changing data.

Most of the time you know what components you need for your app at “design time.”

As you’re writing the code and markup for your site, you know you need a NavBar, so you create a NavBar component and render it on the page.

<div>
    <NavBar />
</div>

This makes it very easy to look at your code and see which components are going to be rendered when you run it in the browser.

However, there are times you don’t necessarily know up front which components you need to show on the page.

Take for example, a customizable dashboard. You know the sort—a page that brings together lots of different information like graphs, tables, maybe a form or two.

The key word here is “customizable.” If you’re just building a fixed dashboard that looks the same for everyone, you can stick to the hardcoded approach we mentioned before.

But if users are to customize their own dashboards, you’re probably going to need a different approach.

Dynamic Dashboards

The requirement is to take some arbitrary components and render those on the page.

In this case, we can imagine that we might store a user’s custom dashboard configuration using a structure like this:

DashboardConfig.cs

public class DashboardConfig {
    public List<Panel> Panels { get; set; }
}

public class Panel {
    public string Title { get;set; }
}

We can then iterate over the list of panels in a Blazor component.

Dashboard\Index.razor

@foreach(var panel in dashboard.Panels){
	<div>
    	<h1>
            @panel.Title
        </h1>
        <div>
            ...
        </div>
	</div>
}

@code {

	DashboardConfig dashboard = ...;

}

So far so good, but what about those custom components we mentioned?

DynamicComponent to the Rescue

Blazor, as of .NET 6, has a very handy DynamicComponent component for this exact purpose:

<DynamicComponent Type="@typeof(Counter)"/>

So long as we know the type of the component we want to render, DynamicComponent will do the rest.

Let’s update our Dashboard model to include a component type:

DashboardConfig.cs

public class DashboardConfig {
    public List<Panel> Panels { get; set; }
}

public class Panel {
    public string Title { get;set; }
    public Type Component { get;set; }
}

Now we can modify our markup to render this component using DynamicComponent.

Index.razor (markup)

@foreach(var panel in dashboard.Panels){
	<div>
    	<h1>
            @panel.Title
        </h1>
        <div>
            <DynamicComponent Type="@panel.Component" />
        </div>
	</div>
}

We can quickly test this by creating an instance of DashboardConfig in code:

Index.razor (@code)

@code {

	DashboardConfig dashboard = new DashboardConfig { 
        Panels = new List<Panel>{ 
            new Panel { Title = "Counter", Component = typeof(Counter) },
            new Panel { Title = "Weather", Component = typeof(FetchData) }
        }};

}

Just like that, we have a dashboard!

At this point, you might want to swap out your dashboard panel markup for a better looking component. If you’re using a component library like Telerik UI for Blazor, it’s probably worth taking a moment to find a component which you can drop in to act as a dashboard panel.

If you’re using the default Blazor project templates, you can use Bootstrap’s card classes to get to something that looks a little better in the browser:

@foreach (var panel in dashboard.Panels)
{
    <div class="card my-4">
        <h1 class="card-header">
            @panel.Title
        </h1>
        <div class="card-body">
            <div class="card-text">
                <DynamicComponent Type="@panel.Component" />
            </div>
        </div>
    </div>
}

Here’s how it looks in the browser:

Dynamic Dashboard: Snapshot of a dashboard with two panels. One panel contains an instance of the standard Blazor Counter component, the other an instance of the FetchData component

Making the Dashboard a Little More Resilient

This is a good first step, but how would we actually store this dashboard config in a database?

For that we’ll need to store the name of the Type (as we can’t store a Type directly in the DB).

Let’s start by modifying DashboardConfig to use a string for the component (instead of a Type).

DashboardConfig.cs

public class DashboardConfig {
    public List<Panel> Panels { get; set; }
}

public class Panel {
    public string Title { get;set; }
    public string Component { get;set; }
}

At this point, we’ll run into a compile error as we’re trying to assign a Type to a string when we create our test data. Let’s switch that code from typeof to nameof instead.

Index.razor (@code)

@code {

    DashboardConfig dashboard = new DashboardConfig
    {
        Panels = new List<Panel>
        {
            new Panel { Title = "Counter", Component = nameof(Counter)},
            new Panel { Title = "Weather", Component = nameof(FetchData)}
        }
    };

}

Now we’re simply grabbing the name of the component and assigning it (as a string) to our panel’s component property.

But if we try to run this we’ll bump into a new error.

error CS1503: Argument 1: cannot convert from 'string' to 'System.Type' 

The problem is we’re now passing the name of the component around, but DynamicComponent wants a type.

We also need to handle the possibility that we’ll store the name of a component in the system only to find, at some later date, that said component has been renamed or deleted and can no longer be rendered.

Happily, we can solve both problems with a small tweak to our code.

First, in the @code for our dashboard we can create a handy helper function to resolve the component type from a string.

Index.razor (@code)

@code {
    ...
    
    private Type? ResolveComponent(string componentName)
    {
        return string.IsNullOrEmpty(componentName) ? null
            : Type.GetType($"{App Namespace}.Widgets.{componentName}");
    }
}

This assumes the components are located in a Widgets folder in the root of the app, and that we’ll replace {App Namespace} with the actual namespace for the app.

Now we can use this function in our markup to render the component (only when we can resolve its type).

Index.razor (markup)

<div class="card my-4">
    <h1 class="card-header">
        @panel.Title
    </h1>
    <div class="card-body">
        <div class="card-text">
            @{
                var componentType = ResolveComponent(@panel.Component);
                if (componentType != null)
                {
                	<DynamicComponent Type="componentType"/>
                }
            }
        </div>
    </div>
</div>

From here we can persist that configuration and load it again when a user attempts to access the dashboard.

Passing Data to Dynamic Components

You may also want to provide extra configuration for a component beyond just its type.

In this dashboard example, you can imagine users wanting to save extra parameters (like a date range or a specific product category for a widget that shows sales).

The challenge is to be able to store an arbitrary collection of data, then pass it along to the component when we render it.

DynamicComponent allows for this by accepting a dictionary of parameters of type IDictionary<string, object>.

Let’s update our DashboardConfig.Panel model to store a dictionary of parameters.

DashboardConfig.cs

public class Panel
{
    public string Title { get; set; }
    public string Component { get; set; }
    public Dictionary<string, object> Parameters { get; set; } = new();
}

Now we can use this Parameters dictionary to store configuration for any of our dashboard components.

Index.razor (@code)

DashboardConfig dashboard = new DashboardConfig
{
    Panels = new List<Panel>
    {
        new Panel
        {
            Title = "Counter",
            Component = nameof(Counter)
        },
        new Panel
        {
            Title = "Weather",
            Component = nameof(FetchData),
            Parameters = new Dictionary<string, object> { ["StartDate"] = new DateTime(2022,2,1) }
        }
    }
};

FetchData in this case accepts a StartDate parameter.

Widgets\FetchData.razor

[Parameter]
public DateTime StartDate { get; set; } = DateTime.Now;

The last step is to pass these parameters along to DynamicComponent.

<div class="card-text">
    @{
        var componentType = ResolveComponent(@panel.Component);
        if (componentType != null)
    	{
    		<DynamicComponent Type="componentType" Parameters="@panel.Parameters"/>
    	}
    }
</div>

From here we can persist that dashboard configuration to a database (or any other storage option) for each user and we have our user customizable dashboard!

Have you tried the Telerik REPL for Blazor? It’s a new web-based tool for the Blazor community that makes it quick and easy to write, run, save and share code snippets. Check it out!

Jon Hilton
About the Author

Jon Hilton

Jon spends his days building applications using Microsoft technologies (plus, whisper it quietly, a little bit of JavaScript) and his spare time helping developers level up their skills and knowledge via his blog, courses and books. He's especially passionate about enabling developers to build better web applications by mastering the tools available to them. Follow him on Twitter here.

Related Posts

Comments

Comments are disabled in preview mode.