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.buttonButtonClick, 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.