What happens if you don't know how your page should look at development time and its content and page controls should be shown based on data that can be changed dynamically by a user or a site administrator? You need to generate your page dynamically.
Introduction
If you ask me why I am so excited about this technology, I will answer - well, it’s because I hate JavaScript (like many others), and this time Microsoft delivered something so useful and powerful for Web developers, that now it becomes a real alternative to JavaScript and all these bloatware frameworks created to extend its pointless life (Angular, React, etc.)
Working at my current place (Pro Coders), we used Blazor from its preview release and tried many amazing things, mostly with Blazor Server-Side that is built on top of SignalR. I should also mention that the technique I am going to share today can be used for client-side Blazor WebAssembly too.
If you are interested there is another article about dynamic forms:
Dynamic Content
As you may know, to define Blazor UI, we use Razor pages - technology that was around for about 5 years, and I will not waste your time digging into Razor, assuming it is simple and well known for you.
What happens if you don't know how your page should look at development time and its content and page controls should be shown based on data that can be changed dynamically by a user or a site administrator? You need to generate your page dynamically.
Let's talk about an example from real life- content management systems where the site administrator can decide which fields can be populated by a user in the user profile page and let me formulate a user story that we will implement today.
User Story #1: Dynamic UI Generation
- Generate a UI page based on a control list received from a service
- Support two types of controls:
TextEdit
and DateEdit
- Control list has properties for UI generation:
Label
, Type
, Required
Implementation - Create a Project
Let's create a new Blazor project and let's keep it simple. Open Visual Studio 2019 and click Create a new project, then find Blazor template and click Next:
Enter the project name, select Blazor Server App on the next page, and click Create:
You will see a new solution created for you and it will contain several pages that Visual Studio template added for learning purposes. You can build and run the solution to see the created application in a browser, by clicking on the play button (green triangle).
Implementation - Define Model and Service
I prefer starting from the defining model, so create a new class ControlDetails.cs and put the following code into it:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoDynamicContent
{
public class ControlDetails
{
public string Type { get; set; }
public string Label { get; set; }
public bool IsRequired { get; set; }
}
}
Now we can create a service class that, for now, will return some test data, so create ControlService.cs and add this code:
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 });
return result;
}
}
}
In this code, we specify three controls that we want to show on our dynamic page: First Name, Last Name, and Birth Date.
The last bit that we need to do is to register our service for Dependency Injection, so simply open Startup.cs, locate [ConfigureServices(IServiceCollection services)
] method and add a line of code at the bottom, so it looks like:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
services.AddSingleton<ControlService>();
}
Implementation - Dynamic page
Let's reuse Counter.razor
page (already created by Visual Studio template) for simplicity. We need to delete all lines except the first one and start adding our own code, firstly use Dependency Injection to inject our service:
page "/counter"
@inject ControlService _controlService
Now we need to execute _controlService
and iterate through the returned list of controls.
@page "/counter"
@inject ControlService _controlService
@foreach (var control in _controlService.GetControls())
{
}
For each control, we will want to show a label that is marked by *
if it is a required control:
@page "/counter"
@inject ControlService _controlService
@foreach (var control in _controlService.GetControls())
{
@if (control.IsRequired)
{
<div>@(control.Label)*</div>
}
else
{
<div>@control.Label</div>
}
}
The final bit is to render control of a particular type in a switch
statement:
@page "/counter"
@inject ControlService _controlService
@foreach (var control in _controlService.GetControls())
{
@if (control.IsRequired)
{
<div>@(control.Label)*</div>
}
else
{
<div>@control.Label</div>
}
@switch (control.Type)
{
case "TextEdit":
<input required="@control.IsRequired">
break;
case "DateEdit":
<input required="@control.IsRequired" type="date">
break;
}
}
Implementation - Running
Now you can compile and run your solution. On the appeared browser window, click Counter menu item to see the result:
Implementation - Adding Control Binding
Extending this idea further, we will need to bind generated razor controls to properties in our razor page and store them in a Dictionary
for example, where Key
is Label
and Value
is the razor control value.
Here, I added the @code
section that has service execution logic and all the properties and events that we bind to controls. The bindings work in both directions.
The resulting code will look like:
@page "/counter"
@inject ControlService _controlService
@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;
}
}
<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
}
}
If you run your solution now, having a breakpoint in the OnClick
method, then enter values in page controls, and click the Submit button, you will see the entered values in the watch panel at the bottom:
It is brilliant, we store entered values in a Dictionary
and now can supply it to a service that saves values to a database.
The full solution with the resulting code can be found on my GitHub page:
Summary
In this exercise, we implemented a UI page that generates controls using data received from a service. The Data controls the Presentation. Supplying data stored in a database, we present content to users. If we want to present slightly different content - we need to only change the data in our database and users see our changes, no recompilation, or deployment required.
But this solution has a small limitation - it will support only those controls that we specify in the [switch
] statement on the razor page. Each time we need to show a control that is not specified in the [switch
] statement, we need to extend it, adding code with new control and recompile solution.
Next Challenge - Custom Controls for Dynamic Content
Is it possible 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 our dynamic page?
Yes, it is possible - using a technique that is shared in my next blog.
There is a similar apporach uisng open-source library:
Thank you and see you next time!
History
- 12th October, 2020: Initial version