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.
The repository for the articles has moved to Blazor.Database Repository. All previous repos are obselete and will be removed shortly.
There's a SQL script in /SQL in the repository for building the database.
The demo site has changed now the Server and WASM have been combined. The site starts in Server mode - https://cec-blazor-database.azurewebsites.net/.
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 are application specific. The only difference between a RouteView and a Form is a RouteView declares one or more routes through the @Page
directive - or directly as a [RouteAttribute]
on a class. On the Router
component declared in the root App
, AppAssembly
specifies the assembly that Router
trawls on initialization to find all the declared routes.
In the application RouteViews are declared in the WASM application library and are common to both WASM and Server SPAs.
The Weather Forecast Viewer and List Views are shown below.
// Blazor.Database/RouteViews/Weather/WeatherViewer.cs @page "/weather/view/{ID:Guid}" @namespace Blazor.Database.RouteViews <WeatherForecastViewerForm ID="this.ID" ExitAction="this.ExitToList"></WeatherForecastViewerForm> @code { [Parameter] public Guid ID { get; set; } [Inject] public NavigationManager NavManager { get; set; } private void ExitToList() => this.NavManager.NavigateTo("/fetchdata"); }
// Blazor.Database/RouteViews/Weather/FetchData.cs @page "/fetchdata" @namespace Blazor.Database.RouteViews <WeatherForecastListForm EditRecord="this.GoToEditor" ViewRecord="this.GoToViewer" NewRecord="this.GoToNew" ExitAction="Exit"></WeatherForecastListForm> @code { [Inject] NavigationManager NavManager { get; set; } private bool _isWasm => NavManager?.Uri.Contains("wasm", StringComparison.CurrentCultureIgnoreCase) ?? false; public void GoToEditor(Guid id) => this.NavManager.NavigateTo($"weather/edit/{id}"); public void GoToNew() => this.NavManager.NavigateTo($"weather/edit/{Guid.Empty}"); public void GoToViewer(Guid id) => this.NavManager.NavigateTo($"weather/view/{id}"); public void Exit() { if (_isWasm) this.NavManager.NavigateTo($"/wasm"); else this.NavManager.NavigateTo($"/"); } }
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/Forms/WeatherForecast/WeatherForecastViewerForm.razor @namespace Blazor.Database.Forms @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.
// Blazor.Database/Forms/WeatherForecast/WeatherForecastViewerForm.razor.cs public partial class WeatherForecastViewerForm : RecordFormBase<WeatherForecast> { [Inject] private WeatherForecastViewService ViewService { get; set; } protected async override Task OnInitializedAsync() { this.Service = this.ViewService; await base.OnInitializedAsync(); } }
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 UIComponent
.
AppComponentBase
inherits from ComponentBase
and adds functionality to manage splatter attributes and the Childcontent
render fragment.
public class AppComponentBase : ComponentBase { [Parameter] public RenderFragment ChildContent { get; set; } [Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> UserAttributes { get; set; } = new Dictionary<string, object>(); protected virtual List<string> UnwantedAttributes { get; set; } = new List<string>(); protected Dictionary<string, object> SplatterAttributes { get { var list = new Dictionary<string, object>(); foreach (var item in UserAttributes) { if (!UnwantedAttributes.Any(item1 => item1.Equals(item.Key))) list.Add(item.Key, item.Value); } return list; } } }
UIComponent
inherits from AppComponentBase
. It builds an HTML DIV block that you can turn on or off.
Lets look at some of UIComponent
in detail.
The HTML block tag can be set using the Tag
parameter. It can only be set by inherited classes.
[Parameter] public string Tag { get; set; } = null; protected virtual string HtmlTag => this.Tag ?? "div";
The control Css class is built using a CssBuilder
class. Inheriting classes can add Css to the CssClasses
collection. External css can be set using the class
attribute on the control.
protected virtual List<string> CssClasses { get; private set; } = new List<string>(); protected string CssClass => CSSBuilder.Class() .AddClass(CssClasses) .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 EventCallback<MouseEventArgs> ClickEvent { get; set; }
Finally the control sets the attributes to remove from the splatter attributes.
protected override List<string> UnwantedAttributes { get; set; } = new List<string>() { "class" };
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"); if (ClickEvent.HasDelegate) builder.AddAttribute(3, "onclick", EventCallback.Factory.Create<MouseEventArgs>(this, ClickEvent)); builder.AddMultipleAttributes(3, this.SplatterAttributes); builder.AddContent(4, ChildContent); builder.CloseElement(); } }
The rest of the article looks at a few of the UI controls in more detail.
This is a standard Bootstrap Button.
Type
is set through the type
attribute.class
attribute.button
ButtonClick
, Show
and Disabled
are handled by the base component.// Blazor.SPA/Components/UIComponents/Forms/UIButtons.cs public class UIButton : UIComponent { public UIButton() => this.CssClasses.Add("btn mr-1"); protected override string HtmlTag => "button"; }
Here's some code showing the control in use.
<UIButton Show="true" Disabled="this._dirtyExit" class="btn-dark" ClickEvent="() => this.Exit()">Exit</UIButton>
This is a wrapper control designed to save implementing error checking in child content. It only renders child content when State
is Loaded
. It displays alternative content when the view is loading or in error.
@namespace Blazor.SPA.Components @inherits ComponentBase @if (this.State == ComponentState.Loaded) { @this.ChildContent } else if (this.State == ComponentState.InError) { if (this.ErrorContent != null) { @this.ErrorContent } else { <div class="m-2 p-2">Error Loading Content</div> } } else { if (this.LoadingContent != null) { @this.LoadingContent } else { <div class="m-2 p-2">Loading......</div> } } @code{ [Parameter] public RenderFragment ChildContent { get; set; } [Parameter] public RenderFragment LoadingContent { get; set; } [Parameter] public RenderFragment ErrorContent { get; set; } [Parameter] public ComponentState State { get; set; } = ComponentState.Loaded; }
You can see the control in use in the Edit and View forms.
These controls create the BootStrap grid system - i.e. container, row and column - by building out DIVs with the correct Css.
public class UIContainer : UIComponent { public UIContainer() => CssClasses.Add("container - fluid"); }
class UIRow : UIComponent { public UIRow() => CssClasses.Add("row"); }
public class UIColumn : UIComponent { [Parameter] public virtual int Cols { get; set; } = 0; public UIColumn() => CssClasses.Add(this.Cols > 0 ? $"col-{this.Cols}" : $"col"); }
public class UILabelColumn : UIColumn { [Parameter] public override int Cols { get; set; } = 2; [Parameter] public string FormCss { get; set; } = "form-label"; public UILabelColumn() => this.CssClasses.Add(this.FormCss); }
Here's some code showing the controls in use.
<UIContainer> <UIRow> <UILabelColumn Columns="2"> Date </UILabelColumn> ............ </UIRow> .......... </UIContainer>
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:
UILoader
, just make life easier!Check the readme in the repository for the latest version of the article set.