This article describes how to build reusable UI Controls from components for use in the Presentation/UI layer of a Blazor Database Application.
Introduction
This article is the fourth in a series on Building Blazor Database Applications. This article looks at the components we use in the UI and then focuses on how to build generic UI Components from HTML and CSS.
- Project Structure and Framework.
- Services - Building the CRUD Data Layers.
- View Components - CRUD Edit and View Operations in the UI.
- UI Components - Building HTML/CSS Controls.
- View Components - CRUD List Operations in the UI.
Repository and Database
The repository for the articles has move to CEC.Blazor.SPA Repository. CEC.Blazor GitHub Repository is obselete and will be removed.
There's a SQL script in /SQL in the repository for building the database.
You can see the Server and WASM versions of the project running here on the same site.
Components
For a detailed look at components read my article A Dive into Blazor Components.
Everything in the Blazor UI, other than the start page, is a component. Yes App, Router,... they're all components. Not all components emit Html.
You can divide components into four categories:
- RouteViews - these are the top level components. Views are combined with a Layout to make the display window.
- Layouts - Layouts combine with Views to make up the display window.
- Forms - Forms are logical collections of controls. Edit forms, display forms, list forms, data entry wizards are all classic forms. Forms contain controls - not HTML.
- Controls - Controls either display something - emit HTML - or do some unit of work. Text boxes, dropdowns, buttons, grids are all classic Hrtml emitting controls. App, Router, Validation are controls that do units of work.
RouteViews
RouteViews are application specific, the only difference between a RouteView and a Form is a RouteView declares one or more routes through the @Page
directive. The Router
component declared in the root App
sets the AppAssembly
to a secific code assembly. This is the assembly that Router
trawls though on startup to find all the declared routes.
In the application RouteViews are decalred in the WASM application library.
The Weather Forecast Viewer and List Views are shown below.
@page "/weather/view/{ID:int}"
<WeatherForecastViewerForm ID="this.ID" ExitAction="this.ExitToList"></WeatherForecastViewerForm>
@code {
[Parameter] public int ID { get; set; }
[Inject] public NavigationManager NavManager { get; set; }
private void ExitToList()
=> this.NavManager.NavigateTo("/fetchdata");
}
@page "/fetchdata"
<WeatherForecastComponent></WeatherForecastComponent>
Forms
We saw Forms in the last article. They're specific to the application.
The code below shows the Weather Viewer. It's all UI Controls, no HTML markup.
// Blazor.Database/Components/Forms/WeatherForecastViewerForm.razor
@namespace Blazor.Database.Components
@inherits RecordFormBase<WeatherForecast>
<UIContainer>
<UIFormRow>
<UIColumn>
<h2>Weather Forecast Viewer</h2>
</UIColumn>
</UIFormRow>
</UIContainer>
<UILoader Loaded="this.IsLoaded">
<UIContainer>
<UIFormRow>
<UILabelColumn>
Date
</UILabelColumn>
<UIInputColumn Cols="3">
<InputReadOnlyText Value="@this.ControllerService.Record.Date.ToShortDateString()"></InputReadOnlyText>
</UIInputColumn>
<UIColumn Cols="7"></UIColumn>
</UIFormRow>
<UIFormRow>
<UILabelColumn>
Temperature °C
</UILabelColumn>
<UIInputColumn Cols="2">
<InputReadOnlyText Value="@this.ControllerService.Record.TemperatureC.ToString()"></InputReadOnlyText>
</UIInputColumn>
<UIColumn Cols="8"></UIColumn>
</UIFormRow>
<UIFormRow>
<UILabelColumn>
Temperature °f
</UILabelColumn>
<UIInputColumn Cols="2">
<InputReadOnlyText Value="@this.ControllerService.Record.TemperatureF.ToString()"></InputReadOnlyText>
</UIInputColumn>
<UIColumn Cols="8"></UIColumn>
</UIFormRow>
<UIFormRow>
<UILabelColumn>
Summary
</UILabelColumn>
<UIInputColumn Cols="9">
<InputReadOnlyText Value="@this.ControllerService.Record.Summary"></InputReadOnlyText>
</UIInputColumn>
</UIFormRow>
</UIContainer>
</UILoader>
<UIContainer>
<UIFormRow>
<UIButtonColumn>
<UIButton AdditionalClasses="btn-secondary" ClickEvent="this.Exit">Exit</UIButton>
</UIButtonColumn>
</UIFormRow>
</UIContainer>
The code behind page is relatively simple - the complexity is in the boilerplate code in the parent classes. It loads the record specific Controller service.
public partial class WeatherForecastViewerForm : RecordFormBase<WeatherForecast>
{
[Inject] private WeatherForecastControllerService ControllerService { get; set; }
protected async override Task OnInitializedAsync()
{
this.Service = this.ControllerService;
await base.OnInitializedAsync();
}
}
UI Controls
UI Controls emit HTML and CSS markup. All the controls here are based on the Bootstrap CSS Framework. All controls inherit from ComponentBase
and UI Controls inherit from UIBase
.
UIBase
UIBase
inherits from Component
. It builds an HTML DIV block that you can turn on or off.
Lets look at some of UIBase
in detail.
The HTML block tag can be set using the Tag
parameter. It can only be set by inherited classes.
protected virtual string HtmlTag => "div";
The control Css class is built using a CssBuilder
class. Inheriting classes can set a primary Css value and add as many secondary values they wish. Add on CSS classes can be added either through the AdditionalClasses
parameter or by defining a class
attribute.
[Parameter] public virtual string AdditionalClasses { get; set; } = string.Empty;
protected virtual string PrimaryClass => string.Empty;
protected List<string> SecondaryClass { get; private set; } = new List<string>();
protected string CssClass
=> CSSBuilder.Class(this.PrimaryClass)
.AddClass(SecondaryClass)
.AddClass(AdditionalClasses)
.AddClassFromAttributes(this.UserAttributes)
.Build();
The control can be hidden or disabled with two parameters. When Show
is true ChildContent
is displayed. When Show
is false HideContent
is displayed if it isn't null
, otherwise nothing is displayed.
[Parameter] public bool Show { get; set; } = true;
[Parameter] public bool Disabled { get; set; } = false;
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public RenderFragment HideContent { get; set; }
Finally the control captures any additional attributes and adds them to the markup element.
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object> UserAttributes { get; set; } = new Dictionary<string, object>();
The control builds the RenderTree
in code.
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (this.Show)
{
builder.OpenElement(0, this.HtmlTag);
if (!string.IsNullOrWhiteSpace(this.CssClass)) builder.AddAttribute(1, "class", this.CssClass);
if (Disabled) builder.AddAttribute(2, "disabled");
builder.AddMultipleAttributes(3, this.UserAttributes);
if (this.ChildContent != null) builder.AddContent(4, ChildContent);
else if (this.HideContent != null) builder.AddContent(5, HideContent);
builder.CloseElement();
}
}
Some Examples
The rest of the article looks at a few of the UI controls in more detail.
UIButton
This is a standard Bootstrap Button.
Type
sets the button type. PrimaryClass
is set. ButtonClick
handles the button click event and calls the EventCallback. Show
and Disabled
handle button state.
@namespace Blazor.SPA.Components
@inherits UIBase
@if (this.Show)
{
<button class="@this.CssClass" @onclick="ButtonClick" type="@Type" disabled="@this.Disabled" @attributes="UserAttributes">
@this.ChildContent
</button>
}
@code {
[Parameter] public string Type { get; set; } = "button";
[Parameter] public EventCallback<MouseEventArgs> ClickEvent { get; set; }
protected override string PrimaryClass => "btn mr-1";
protected async Task ButtonClick(MouseEventArgs e) => await this.ClickEvent.InvokeAsync(e);
}
Here's some code showing the control in use.
<UIButton Show="true" Disabled="this._dirtyExit" AdditionalClasses="btn-dark" ClickEvent="() => this.Exit()">Exit</UIButton>
UIColumn
This is a standard Bootstrap Column.
Cols
defines the number of columns PrimaryCss
is built fromCols
. Base
RenderTreeBuilder
builds out the control as a div.
public class UIColumn : UIBase
{
[Parameter] public virtual int Cols { get; set; } = 0;
protected override string PrimaryClass => this.Cols > 0 ? $"col-{this.Cols}" : $"col";
}
UILoader
This is a wrapper control designed to save implementing error checking in child content. It only renders it's child content when IsLoaded
is true. The control saves implementing a lot of error checking in the child content.
@namespace Blazor.SPA.Components
@inherits UIBase
@if (this.Loaded)
{
@this.ChildContent
}
else
{
<div>Loading....</div>
}
@code {
[Parameter] public bool Loaded { get; set; }
}
You can see the control in use in the Edit and View forms.
UIContainer/UIRow/UIColumn
These controls create the BootStrap grid system - i.e. container, row and column - by building out DIVs with the correct Css.
public class UIContainer : UIBase
{
protected override string PrimaryClass => "container-fluid";
}
class UIRow : UIBase
{
protected override string PrimaryClass => "row";
}
public class UIColumn : UIBase
{
[Parameter] public virtual int Cols { get; set; } = 0;
protected override string PrimaryClass => this.Cols > 0 ? $"col-{this.Cols}" : $"col";
}
public class UILabelColumn : UIColumn
{
protected override string _BaseCss => $"col-{Columns} col-form-label";
}
Here's some code showing the controls in use.
<UIContainer>
<UIRow>
<UILabelColumn Columns="2">
Date
</UILabelColumn>
............
</UIRow>
..........
</UIContainer>
Wrap Up
This article provides an overview on how to build UI Controls with components, and examines some example components in detail. You can see all the library UIControls in the GitHub Repository
Some key points to note:
- UI Controls let you abstract markup from higher level components such as Forms and Views.
- UI Controls give you control and applies some discipline over the HTML and CSS markup.
- View and Form components are much cleaner and easier to view.
- Use as little or as much abstraction as you wish.
- Controls, such as
UILoader
, just make life easier!
If you're reading this article well into the future, check the readme in the repository for the latest version of the article set.
History
* 21-Sep-2020: Initial version.
* 17-Nov-2020: Major Blazor.CEC library changes. Change to ViewManager from Router and new Component base implementation.
* 29-Mar-2021: Major updates to Services, project structure and data editing.