How to Build a Blazor Business Screen with Custom Editors
A business grid becomes interesting when a cell is more than a string. An employee might refer to another employee as a manager, a purchase might refer to a supplier, and a row might expose an application command that needs to complete asynchronously. These cases need an editing workflow, not just a table with a few input elements attached to live objects.
Sphere10's Blazor grid combines typed column definitions with buffered row editing and a shared data-source contract. This example adds a Notes editor, an existing-manager picker and a Details command to a small people screen. The complete sample includes the model, validating data source and minimal Interactive Server host. The page below is the presentation layer that consumes them.
Contents
Keep Persistence in the Data Source
The sample's PeopleDataSource derives from ListDataSource<Person> and owns searching, sorting, paging and mutations. It validates required names, age limits and the rule that a person cannot be their own manager. The validation is enforced in mutation methods too, so a caller outside the grid cannot bypass it merely by avoiding the visible Save button.
For the tutorial, the list is page-local and memory-backed. Refreshing into a new page instance recreates the demonstration data. That limitation is useful to state because the same grid can later consume a persistent source. The UI does not become a database just because it supports CRUD operations. Database or HTTP work belongs in an appropriate asynchronous source rather than being hidden inside a blocking UI callback.
Configure a Custom Editing Page
@page "/people-custom"
@rendermode InteractiveServer
@using System.Collections.Generic
@using System.Threading.Tasks
@using Microsoft.AspNetCore.Components
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using ArticleSample.Models
@using ArticleSample.Services
@using Sphere10.Framework.Web.AspNetCore.Blazor
@using Sphere10.Framework.Web.AspNetCore.Blazor.UI.Controls.BlazorGrid
<h1>People with custom editors</h1>
<p role="status">@_message</p>
<BlazorGrid TItem="Person" DataSource="_source" Columns="_columns" Actions="_actions" PageSize="2" />
@code {
private readonly PeopleDataSource _source = new();
private BlazorGridColumn<Person>[] _columns = Array.Empty<BlazorGridColumn<Person>>();
private readonly BlazorGridColumn<Person>[] _managerColumns = new[] {
BlazorGridColumn<Person>.For(person => person.Id, "ID"),
BlazorGridColumn<Person>.For(person => person.Name)
};
private GridAction<Person>[] _actions = Array.Empty<GridAction<Person>>();
private string _message = "Edit a person to choose their manager.";
protected override void OnInitialized() {
var name = BlazorGridColumn<Person>.For(person => person.Name);
name.ExpandsToFit = true;
name.Template = person => @<strong>@person.Name</strong>;
var notes = BlazorGridColumn<Person>.For(person => person.Notes);
notes.EditorTemplate = context => @<input aria-label="Notes" maxlength="120"
value="@context.Value" @onchange="args => context.ValueChanged.InvokeAsync(args.Value)" />;
var manager = new BlazorGridColumn<Person> {
ColumnName = "Manager",
PropertyName = nameof(Person.Manager),
DataType = typeof(Person),
PropertyValue = person => person.Manager,
SetPropertyValue = (person, value) => person.Manager = (Person)value,
CanEditCell = true,
Width = 220,
Template = person => @<span>@(person.Manager?.Name ?? "(None)")</span>,
EditorTemplate = context => @<BlazorGridReferencePicker TItem="Person"
DataSource="_source" Columns="_managerColumns" Value="@(context.Value as Person)"
ValueChanged="person => context.ValueChanged.InvokeAsync(person)"
DisplayText="ManagerText" Label="manager" AllowNull="true" />
};
_columns = new[] { BlazorGridColumn<Person>.For(person => person.Id, "ID"), name, notes, manager };
_actions = new[] { new GridAction<Person>("Details", person => person, string.Empty) {
ActionWorkAsync = ShowDetailsAsync,
IsActionAvailable = person => person.IsActive
} };
}
private static string ManagerText(Person person) {
return $"{person.Name} (ID {person.Id})";
}
private Task ShowDetailsAsync(Person person) {
_message = $"{person.Name}, age {person.Age}.";
return Task.CompletedTask;
}
}
The ordinary Name column uses a typed expression. Its display template emphasises the current name, while editing still follows the normal row workflow. The Notes editor receives a cell edit context and forwards its changed value through ValueChanged. It does not directly mutate context.Item.Notes, which would escape the draft and make Cancel misleading.
The manager column supplies explicit value accessors because its data type is a related Person object. The display template shows a shallow label rather than recursively rendering the entire manager graph. That is important when relationships can form cycles or when loading a related object is expensive.
Choose an Existing Related Record
BlazorGridReferencePicker<Person> receives the full eligible data source and compact columns describing the choices. It must not receive only the parent grid's currently visible page because a valid manager on another page would disappear from the selection universe. The picker masks mutation commands because its job is to select an existing record, rather than edit that record in place.
The selected value returns to the buffered cell context. Selecting a manager is therefore not a database write. The parent's eventual Save or Cancel determines whether that draft relationship is accepted. AllowNull permits clearing a relationship when the application's rules allow it. Server-side validation and authorization still apply.
Treat Actions as Application Commands
The Details action is enabled for active people and uses an awaited ActionWorkAsync callback. In a larger application, that callback might open a registered screen or call an authorized application service. It is not automatically a CRUD update, so it does not acquire normal update permissions or validation merely because it appears beside Save and Delete.
Keep application commands explicit and observable. Return their tasks instead of launching unobserved work. Let failures reach the grid's error workflow and the application's logging owner. An enabled button expresses presentation state. The called service still decides what the current user may do.
Host It Interactively
The complete project registers Razor components and Interactive Server services, builds the Sphere10 Blazor application services, maps static assets and maps the component endpoints. The page explicitly selects InteractiveServer. Registering services alone does not make a statically rendered component interactive. Keep the host's isolated styles bundle and Blazor script so the grid's collocated assets can load normally.
Compilation checks the Razor component APIs. Run the complete host and exercise the actual controls: edit Notes and cancel, select another manager and save, reject an empty name, reject a self-manager and invoke Details. Check keyboard selection and the reference dropdown at desktop and phone widths. These interaction checks are a separate boundary from compilation.
Extend the Example Deliberately
Read the custom editor and picker guide and complete grid quick start . The next useful step is to replace the demonstration list with a persistent data service while preserving its validation contract. The Sphere10 Framework overview connects this browser UI to shared application composition, native WinForms controls and embedded storage.
Download the Example
Download the complete NuGet sample project. The archive contains the project, its model files and run instructions. Keep the pinned package version when reproducing the example.
Continue the Series
How to Build a Modular Application for Desktop and Browser
How to Build an Embedded Object Database in C#
How to Give Your C# Objects Commit and Rollback