Registration is the process by which a child component registers itself with a parent component. The actual content is built out by the parent based on data provided by the child.
I've used a Select edit control as a somewhat contrived example. There's no real reason to register select options, but it provides a simple framework to demonstrate the principles.
The Repo for this article is here - Blazr.ComponentRegistration
You will see similar examples of this pattern where the parent component cascades itself and the child components call a register method on the parent, often registering themselves.
A personal view, but I don't believe this is good practice for the following reasons.
The main component uses the Defer component to render it's content.
It looks like:
@ChildContent
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
}
It's purpose is to defer rendering of the actual content until after the Option components have registered. Defer is at the same level in the Render Tree as the Option components, so renders in sequence with them. As it's placed last, it renders it content - provided as a RenderFragment from the parent - last.
Our definition looks like this:
<div class="mb-3"> <OptionSelect @bind-Value="_country"> <Option Id="UK" Value="UK" /> <Option Id="France" Value="France" /> <Option Id="Spain" Value="Spain" /> <Option Id="Portugal" Value="Portugal" /> </OptionSelect> </div>
SetParametersAsync before any rendering takes place.SetParametersAsync only runs once when _hasRegistered is false.OptionBuilder method as the RenderFragment when it calls Register.SetParametersAsync returns a completed Task. It short circuits the lifecycle process: It does nothing so there's no point in running it.@namespace Blazr.ComponentRegistration.Components @using Microsoft.AspNetCore.Components.Rendering @code { private bool _hasRegistered; [Parameter, EditorRequired] public string? Id { get; set; } [Parameter, EditorRequired] public string? Value { get; set; } [CascadingParameter] private Action<RenderFragment>? Register { get; set; } public override Task SetParametersAsync(ParameterView parameters) { // We only need to register once. // We can ignore all subsequent parameter changes, short circuit the lifecycle processes // and return a completed task. if (!_hasRegistered) { parameters.SetParameterProperties(this); // Check we have everything. If not throw an exception. ArgumentNullException.ThrowIfNull(this.Id); ArgumentNullException.ThrowIfNull(this.Value); ArgumentNullException.ThrowIfNull(this.Register); // Register our render fragment this.Register.Invoke(OptionBuilder); _hasRegistered = true; } // Short circuit the life cycle process. We waste processor time doing it for no purpose. return Task.CompletedTask; } //Create the render fragment that is the rendered content private void OptionBuilder(RenderTreeBuilder __builder) { <option value="@this.Id">@this.Value</option> } }
select with the registered RenderFragments until after all the child components have registered.@namespace Blazr.ComponentRegistration.Components <CascadingValue Value="Register" IsFixed> @ChildContent </CascadingValue> <Defer> <select class="form-select" @bind:get="@this.Value" @bind:set="this.SetValue"> @if (this.Value is null) { <option selected disabled value=""> -- Select An Item -- </option> } @foreach (var item in _items) { @item } </select> </Defer> @code { [Parameter] public string? Value { get; set; } [Parameter] public EventCallback<string?> ValueChanged { get; set; } [Parameter] public RenderFragment? ChildContent { get; set; } private List<RenderFragment> _items = new(); private void Register(RenderFragment option) { if (!_items.Contains(option)) _items.Add(option); } private async Task SetValue(string? value) => await this.ValueChanged.InvokeAsync(value); }
@page "/" <PageTitle>Home</PageTitle> <div class="mb-3"> <OptionSelect @bind-Value="_country"> <Option Id="UK" Value="UK" /> <Option Id="France" Value="France" /> <Option Id="Spain" Value="Spain" /> <Option Id="Portugal" Value="Portugal" /> </OptionSelect> </div> <div class="alert alert-primary">Country: @_country</div> @code { private string? _country; }
In more complex situations we can use a data object for the data transfer and a context class to manage registration and collection management.
A simple record or readonly struct value object to hold the option data.
public record OptionData(string Id, string Value);
The context, which in this case just provides the registration process method and exposes a public readonly collection of OptionData object. It provides the functionality we need.
public class OptionContext { private List<OptionData> _items = new List<OptionData>(); public IEnumerable<OptionData> Items => _items.AsEnumerable(); public void Register(OptionData option) { if (!_items.Contains(option)) _items.Add(option); } }
The sole purpose of the component is to register its configuration data. Nothing else. There's no content to output to the DOM.
SetParametersAsync.SetParametersAsync only runs once when _hasRegistered is false.OptionData with the context: it's data, not itself.SetParametersAsync returns a completed Task. It short circuits the lifecycle process: there's no point in running it to do nothing.using Microsoft.AspNetCore.Components; namespace Blazr.ComponentRegistration.Components; public class BlazrOption : ComponentBase { private bool _hasRegistered; [Parameter, EditorRequired] public string? Id { get; set; } [Parameter, EditorRequired] public string? Value { get; set; } [CascadingParameter] private BlazrOptionContext? Context { get; set; } public override Task SetParametersAsync(ParameterView parameters) { // We only need to do anything if we haven't yet registered if (!_hasRegistered) { // Manually get our parameters from the ParameterView var id = parameters.GetValueOrDefault<string>("Id"); var value = parameters.GetValueOrDefault<string>("Value"); this.Context = parameters.GetValueOrDefault<BlazrOptionContext>("Context"); // Check we have everything. If hot throw an exception ArgumentNullException.ThrowIfNull(id); ArgumentNullException.ThrowIfNull(value); ArgumentNullException.ThrowIfNull(Context); // Register this.Context.Register(new(id, value)); _hasRegistered = true; } // Short circuit the Lifecycle process. We're wasting processor time doing it for no purpose. return Task.CompletedTask; } }
The main component creates an instance of the OptionContext and cascades it to the ChildContent - the BlazorOption components. It only does this on the first render. Their only purpose is to register their data.
The Defer component is used as before to defer rendering of the main component content. This time the component builds the option code directly.
@namespace Blazr.ComponentRegistration.Components @if (_firstRender) { <CascadingValue Value="_optionContext" IsFixed> @ChildContent </CascadingValue> } <Defer> <select class="form-select" @bind:get="@this.Value" @bind:set="this.SetValue"> @if (this.Value is null) { <option selected disabled value=""> -- Select An Item -- </option> } @foreach (var item in _optionContext.Items) { <option value="@item.Id">@item.Value</option> } </select> </Defer> @code { [Parameter] public string? Value { get; set; } [Parameter] public EventCallback<string?> ValueChanged { get; set; } [Parameter] public RenderFragment? ChildContent { get; set; } private readonly BlazrOptionContext _optionContext = new(); private bool _firstRender = true; private async Task SetValue(string? value) => await this.ValueChanged.InvokeAsync(value); }
And here's the demo page:
@page "/" <PageTitle>Home</PageTitle> <div class="mb-3"> <BlazrSelect @bind-Value="_country"> <BlazrOption Id="UK" Value="UK"/> <BlazrOption Id="France" Value="France" /> <BlazrOption Id="Spain" Value="Spain" /> <BlazrOption Id="Portugal" Value="Portugal" /> </BlazrSelect> </div> <div class="alert alert-primary">Country: @_country</div> @code { private string? _country; }