Build your own Blazor control as a Razor class library and place it on an HTML5 or Portable display through the BlazorCtrl element.
Reference → Code → Extensions API → Custom Controls → Blazor Control Development
A custom Blazor control is a Razor component compiled into a Razor class library (DLL). The FrameworX web client loads that DLL and runs the component in the browser. Tags reach the component's parameters through the display's CodeBehind.
This page covers building the control: the projects, the rules FrameworX applies, and a template you can download and adapt.
To place and configure the finished control on a display and bind it to tags, see Blazor Control Reference. For a ready solution that uses this template's control, see Blazor Control Example. |
Download BlazorControl-ProgressBar.zip. It contains one solution with two projects:
Project | Role |
|---|---|
| The control: a Razor class library with one component, |
| Optional test bench: a small ASP.NET Core web app that shows the control with fixed values, so you can run and debug it without FrameworX. FrameworX never uses it; do not copy its DLL to FrameworX. |
Validated with FrameworX 10.1.5.
Visual Studio with the ASP.NET and web development workload, and the .NET 8 SDK or later.
A Razor class library. The template targets .NET 8; the FrameworX 10.1.5 web client runs on .NET 10 and loads it.
The control runs only in HTML5 clients (a web browser).
The display can be an HTML5 display or a Portable display. On a Portable display the control appears only at runtime in the browser.
The DLL is deployed to the installation's BlazorControls folder, by default C:\Program Files\Tatsoft\FrameworX\fx-10\HTML5\BlazorControls\.
Follow these rules for every custom Blazor control. Breaking one does not always produce an error: the component can be missing from the list, show default values, or ignore changes.
The component is public. It must be public, not abstract, derive from Microsoft.AspNetCore.Components.ComponentBase (every .razor component does), and have a public parameterless constructor. Only such types are listed when you pick the component.
The DLL is in the BlazorControls folder before you select it. The Designer stores the assembly path relative to fx-10\HTML5\BlazorControls\ and rejects a DLL selected anywhere else with the message "Blazor Control should be located at ...".
The assembly has its own name. FrameworX ships a BlazorControlExample.dll in BlazorControls. Building the template unchanged produces a DLL with the same name, which overwrites it.
Every value FrameworX sets or binds is a public property marked [Parameter].
A value the control changes and FrameworX must receive back has an EventCallback<T> parameter. Invoke it with the new value when the control changes it. In the CodeBehind, BindEvent names that callback in its third argument to make the binding two-way.
The component runs in the browser. FrameworX runs it in Interactive WebAssembly render mode, so it cannot use server-only APIs. The test bench runs it in Interactive Server mode and does not catch this: always test in FrameworX.
Extra stylesheets and scripts are deployed with the DLL. The FrameworX web client already loads Bootstrap (BlazorControls\BlazorBootstrap\bootstrap.min.css). Any other .css or .js file your control needs goes into BlazorControls and is listed in the element's Dependencies field.
Close the Designer before replacing the DLL. It keeps the DLL loaded, and the file cannot be overwritten.
Open BlazorControlSolution.sln. To see the control without FrameworX, set WebUIBlazor as the startup project and run it: the browser opens Home.razor, which renders the component with fixed values.
@page "/" @rendermode InteractiveServer <PageTitle>Home</PageTitle> <ProgressBar TargetProduction="100" CurrentProduction="50" BarColor="blue" @rendermode="InteractiveServer"></ProgressBar> |
Choose an assembly name and a namespace for your control (rule 3). The component's full type name is the namespace plus the component name, for example MyCompany.Controls.ProgressBar.
File | What to change |
|---|---|
| Add |
| Change |
| Update the |
Each value FrameworX sets is a public [Parameter] property. CurrentProduction also goes back to FrameworX: the Restart Simulation button sets it to 0 and invokes CurrentProductionRestarted with the new value (rule 5).
@code {
// Parameter to receive the target production value from the parent component
[Parameter]
public int TargetProduction { get; set; }
// Parameter to receive the current production value from the parent component
[Parameter]
public int CurrentProduction { get; set; }
// Parameter for setting the color of the progress bar (default is blue)
[Parameter]
public string BarColor { get; set; } = "blue";
// Event callback to notify the parent component when the production is restarted
[Parameter]
public EventCallback<int> CurrentProductionRestarted { get; set; }
// Method to reset the production progress
private void Restart()
{
// Logs a message in the browser console using JavaScript (JSInterop)
_ = JSRuntime.InvokeVoidAsync("console.log", "Restart command requested!");
// Reset the current production to zero
CurrentProduction = 0;
// Trigger the event callback to inform the parent component
CurrentProductionRestarted.InvokeAsync(0);
}
} |
Build the solution in Release. The control's DLL is BlazorControlExample\bin\Release\net8.0\<AssemblyName>.dll.
Close the Designer (rule 8), then copy the DLL, and any .css or .js file it needs (rule 7), to C:\Program Files\Tatsoft\FrameworX\fx-10\HTML5\BlazorControls\.
On an HTML5 or Portable display, open Displays / Draw, select Viewer, then BlazorCtrl(Web), choose your DLL from the BlazorControls folder and pick the component. Then bind its parameters in the display's CodeBehind, in BlazorControlLoaded. Both steps are described in Blazor Control Reference. For the template's component, the parameters map as follows:
Parameter | Type | Direction |
|
|---|---|---|---|
|
| Tag to control |
|
|
| Both ways |
|
|
| Tag to control |
|
public async Task BlazorControlLoaded(TBlazorControl control)
{
// Runs once for each Blazor control on the display, after it has loaded.
// Two-way: the third argument names the component's EventCallback that sends changes back to the tag.
control.BindEvent("CurrentProduction", @Tag.CurrentProduction.GetName(), "CurrentProductionRestarted");
// One-way (third argument null): the tag only sends values to the control.
control.BindEvent("TargetProduction", @Tag.TargetProduction.GetName(), null);
control.BindEvent("BarColor", @Tag.BarColor.GetName(), null);
} |
The tags in this example are TargetProduction and CurrentProduction (Integer) and BarColor (Text).
Symptom | Cause | Fix |
|---|---|---|
Message: "Blazor Control should be located at ...". | The DLL was selected from a folder other than | Copy it to |
Your component is not in the list. | It is not public, is abstract, has no public parameterless constructor, or its | Check rule 1, and that the file is compiled into the DLL. |
The control shows only its default values. | No | Bind them in |
A change made in the control does not reach the tag. | The binding is one-way, the callback name is wrong, or the component does not invoke its | Pass the callback name as the third argument of |
The control is visible in the browser but not in the Windows client. | Blazor controls run only in HTML5 clients. | Expected. Open the display in a web browser. |
Styles are missing. | A stylesheet or script is not in | Deploy it and list it (rule 7). |
Copying the DLL fails: "being used by another process". | The Designer has the DLL loaded. | Close the Designer and copy again (rule 8). |
It works in the test bench but not in FrameworX. | The test bench runs the component on the server; FrameworX runs it in the browser. | Remove server-only code (rule 6). |
The control library, file by file, followed by the test bench's own files. The test bench's layout, error page and stylesheets are standard ASP.NET Core template files and are in BlazorControl-ProgressBar.zip.
@using Microsoft.JSInterop
@inject IJSRuntime JSRuntime
<!--
This is a simple Blazor component that displays a progress bar.
The progress bar represents production progress based on a target.
It also includes a "Restart Simulation" button to reset progress.
-->
<div class="ms-2">
<h3>Progress Bar</h3>
<!-- Display the target and current production values -->
<p>Target Production: @TargetProduction</p>
<p>Current Production: @CurrentProduction</p>
<!-- Conditional rendering: If the target is 0 or less, display an empty progress bar -->
@if (TargetProduction <= 0)
{
<div style="width: 100%; background: #ddd; height:30px; border-radius: 5px; position: relative">
<div style="width: 0%; background-color: @(BarColor); height:30px; border-radius: 5px"></div>
</div>
}
else
{
<!--
When the target production is greater than 0,
the progress bar dynamically updates its width based on the production progress.
-->
<div style="width: 100%; background: #ddd; height:30px; border-radius: 5px; position: relative">
<div style="width: @(CurrentProduction * 100 / TargetProduction)%;
background-color: @(BarColor);
height:30px;
border-radius: 5px"></div>
</div>
}
<br />
<!-- A button to restart the simulation (reset the production value) -->
<button type="button" class="btn btn-primary ml-2" @onclick="Restart">
Restart Simulation
</button>
</div>
@code {
// Parameter to receive the target production value from the parent component
[Parameter]
public int TargetProduction { get; set; }
// Parameter to receive the current production value from the parent component
[Parameter]
public int CurrentProduction { get; set; }
// Parameter for setting the color of the progress bar (default is blue)
[Parameter]
public string BarColor { get; set; } = "blue";
// Event callback to notify the parent component when the production is restarted
[Parameter]
public EventCallback<int> CurrentProductionRestarted { get; set; }
// Method to reset the production progress
private void Restart()
{
// Logs a message in the browser console using JavaScript (JSInterop)
_ = JSRuntime.InvokeVoidAsync("console.log", "Restart command requested!");
// Reset the current production to zero
CurrentProduction = 0;
// Trigger the event callback to inform the parent component
CurrentProductionRestarted.InvokeAsync(0);
}
} |
@using Microsoft.AspNetCore.Components.Web |
<Project Sdk="Microsoft.NET.Sdk.Razor">
<!-- Razor class library: the DLL FrameworX loads from fx-10\HTML5\BlazorControls\.
Give your own control a new AssemblyName so it does not overwrite the
BlazorControlExample.dll that FrameworX ships in that folder. -->
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.13" />
</ItemGroup>
</Project> |
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\images\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BlazorControlExample\BlazorControlExample.csproj" />
</ItemGroup>
</Project> |
using WebUIBlazor.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
app.Run(); |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="BlazorBootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="BlazorControlExample.css" />
<link rel="stylesheet" href="WebUIBlazor.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html> |
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router> |
@using System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using static Microsoft.AspNetCore.Components.Web.RenderMode @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using WebUIBlazor @using WebUIBlazor.Components @using BlazorControlExample |
Blazor Control Reference: place the BlazorCtrl element on a display and bind its parameters in the CodeBehind.
Blazor Control Example: a ready solution that uses this template's ProgressBar.
Custom Control API Reference: choose between WPF, Blazor and Portable controls.