Custom Property Components

PropertyComponent delegates rendering to registered Blazor components that are resolved at runtime from DynamicComponentProvider. You can replace the default component for any CLR type and display mode combination, or override a single instance inline without touching global registration.

There are two levels of customization:

  1. One-off override — supply ChildContent on a single PropertyComponent instance. No registration required.
  2. Global registration — register a component type for a CLR type and DisplayMode pair so every matching property uses it automatically.

Both approaches receive a PropertyComponentContext that provides data access, metadata, and UX helpers.


IPropertyComponent

Any Blazor component used as a global property component replacement must implement IPropertyComponent:

public interface IPropertyComponent
{
    PropertyComponentContext PropertyContext { get; }
}

The single member is PropertyContext. In your .razor component, declare it with the [CascadingParameter] attribute — PropertyComponent provides the value as a cascading parameter when it renders the resolved component type.

@implements IPropertyComponent

@code {
    [CascadingParameter]
    public PropertyComponentContext PropertyContext { get; set; } = default!;
}

PropertyComponentContext Reference

PropertyComponentContext is the shared context that bundles dataset access and property metadata. It is the primary API surface for all property components, both built-in and custom.

Properties

Member Type Description
DataSet IDataSet The dataset this property is bound to.
PropertyInfo PropertyInfo Full property info object, including path, metadata, display mode, and label settings.
PropertyMetaData PropertyMetaData Metadata from the server: name, label, data type, and constraints. Shorthand for PropertyInfo.PropertyMetaData.
Path string The resolved path of this property within the dataset. Shorthand for PropertyInfo.Path.
DisplayMode DisplayMode The effective display mode for this component instance. Shorthand for PropertyInfo.DisplayMode.
PropertyPermissionLevel PropertyPermissionLevel The RBAC permission level for this property. Defaults to Write when RBAC is not configured.
OnValueChanged Func<object?, Task>? Optional callback invoked immediately when the component changes the value. Set by PropertyComponent when its ValueChanged parameter has a delegate.

Methods

Get<T>()

Reads the current value of this property from the dataset.

var name = PropertyContext.Get<string>();
var count = PropertyContext.Get<int?>();

Returns null when the value is absent or cannot be cast to T.

Set(value)

Writes a value to the dataset and begins the async sync to the server. This is fire-and-forget from the component's perspective — the call returns immediately and the async operation runs in the background.

PropertyContext.Set(newValue);

Internally calls DataSet.SetAsync, which raises DataSet.Changed and causes any subscribed PropertyComponent instances that share the same path to re-render.

IsReadOnly()bool

Returns true when the property should not be edited. Combines the server-side read-only flag from the dataset with the effective permission level. Use this to disable or hide edit controls.

var disabled = PropertyContext.IsReadOnly();

GetShortMessage()string?

Returns the text of the first validation error or user prompt associated with this property, or null if there are none. Use this to show inline validation feedback.

var error = PropertyContext.GetShortMessage();

GetLabel(bool ignoreShowLabel = false)string?

Returns the formatted display label for the property, or null if ShowLabel is false and ignoreShowLabel is not set.

  • If PropertyMetaData.Label is set, it is used; otherwise falls back to PropertyMetaData.Name.
  • The value is run through LabelFormatter which converts PascalCase names to spaced words (e.g., "PrimaryEmail""Primary Email").
  • Pass ignoreShowLabel: true to retrieve the label text regardless of the ShowLabel setting.
var label = PropertyContext.GetLabel();              // respects ShowLabel
var label = PropertyContext.GetLabel(ignoreShowLabel: true); // always returns text

IsDataGridDisplayMode()bool

Returns true when the component is rendering inside a collection grid. Use this to adjust layout — for example, suppressing labels or using a compact variant of an input.

var compact = PropertyContext.IsDataGridDisplayMode();

DynamicComponentRegistration

DynamicComponentRegistration is a record that maps a string type key and a DisplayMode to a Blazor component type.

public record DynamicComponentRegistration(string Type, DisplayMode DisplayMode, Type ComponentType);
Field Type Description
Type string The CLR type key (e.g., "string", "int", "bool") or a DataType name (e.g., "EmailAddress", "PhoneNumber").
DisplayMode DisplayMode The mode this registration applies to: Edit, View, or Edit | DataGrid.
ComponentType Type The Blazor component type to render. Must implement IPropertyComponent.

Type vs DataType

The Type field serves two purposes:

  • CLR type key — matches any property whose underlying CLR type maps to that key. Examples: "string", "int", "decimal", "bool", "Date", "DateTime".
  • DataType name — matches properties annotated with a specific data type, regardless of the underlying CLR type. Examples: "EmailAddress", "PhoneNumber", "Url", "FullAddress". These registrations take precedence over CLR type registrations during resolution.

Use a DataType key when you want a specialized component only for a semantic subtype of a CLR type — for example, a rich email editor for EmailAddress while leaving plain string properties with the default text editor.


DisplayMode

DisplayMode is a flags enum with three values:

[Flags]
public enum DisplayMode
{
    View     = 1,
    Edit     = 2,
    DataGrid = 4,
}

DataGrid is always combined with either View or Edit (e.g., DisplayMode.Edit | DisplayMode.DataGrid). It signals that the component is rendering inside a collection grid cell.


Resolution Order

When PropertyComponent needs to render a property, DynamicComponentProvider.GetComponentType resolves the component type in this order:

  1. Exact match on (Type, DisplayMode) — including the DataGrid flag.
  2. Same type, DisplayMode with the DataGrid flag stripped — allows a non-grid registration to serve grid cells.
  3. Same type, DisplayMode.Edit base mode — fallback when a combined mode has no match.
  4. Same type, DisplayMode.View base mode — fallback for view-only registrations.
  5. null — no component found; PropertyComponent renders an error indicator.

DataType registrations are checked before CLR type registrations at each step, giving them higher priority.


Registering a Custom Component

Call AddPropertyControlComponents() during service registration to set up the DynamicComponentProvider and all built-in defaults. Add your own DynamicComponentRegistration singletons before or after this call — they are all collected and passed to the provider.

// Program.cs
builder.Services.AddPropertyControlComponents();

builder.Services.AddSingleton(new DynamicComponentRegistration(
    "string",
    DisplayMode.Edit,
    typeof(MyCustomStringEditor)));

Your registration overrides the default because the last registration for a given (Type, DisplayMode) key wins — DynamicComponentProvider stores registrations in a dictionary and later additions replace earlier ones.

To register for a DataType key, use the exact string name:

builder.Services.AddSingleton(new DynamicComponentRegistration(
    nameof(DataType.EmailAddress),
    DisplayMode.Edit,
    typeof(MyEmailEditor)));

Step-by-Step: Creating a Custom Component

a. Create a .razor file that implements IPropertyComponent

@* MyCustomEditor.razor *@
@implements IPropertyComponent

b. Declare [CascadingParameter] PropertyComponentContext PropertyContext

@code {
    [CascadingParameter]
    public PropertyComponentContext PropertyContext { get; set; } = default!;
}

c. Bind inputs using PropertyContext.Get<T>() and PropertyContext.Set(value)

<input value="@PropertyContext.Get<string>()"
       @onchange="e => PropertyContext.Set(e.Value)" />

d. Use IsReadOnly(), GetLabel(), and GetShortMessage() for UX

@if (PropertyContext.GetLabel() is string label)
{
    <label>@label</label>
}

<input value="@PropertyContext.Get<string>()"
       @onchange="e => PropertyContext.Set(e.Value)"
       disabled="@PropertyContext.IsReadOnly()" />

@if (PropertyContext.GetShortMessage() is string error)
{
    <span class="field-error">@error</span>
}

e. Register in DI for the desired CLR type and display mode

builder.Services.AddSingleton(new DynamicComponentRegistration(
    "string",
    DisplayMode.Edit,
    typeof(MyCustomEditor)));

Using ChildContent for One-Off Overrides

When you need to customize the rendering of a single PropertyComponent instance without affecting other properties of the same type, provide a ChildContent render fragment. See PROPERTY_COMPONENTS.md for the basic usage pattern. This section covers the context API details that matter when implementing the override body.

ChildContent is typed as RenderFragment<PropertyComponentContext>. The context variable exposes the full PropertyComponentContext API — the same surface available inside a globally registered component.

<PropertyComponent Path="Notes">
    @* context is a PropertyComponentContext *@
    <MyInlineOverride Context="@context" />
</PropertyComponent>

You do not need to implement IPropertyComponent when using ChildContent. The context is provided directly by the render fragment parameter; there is no cascading parameter involved.

Key differences from a globally registered component:

  • Scope — applies only to this one PropertyComponent instance on this one page.
  • No registration — nothing to add to DI.
  • Full context accessGet<T>(), Set(value), IsReadOnly(), GetLabel(), GetShortMessage(), and IsDataGridDisplayMode() all work identically to a registered component.

Code Examples

Example 1: Custom String Editor (Multi-Line Textarea with Character Counter)

This component replaces the default single-line text field for string properties in edit mode with a multi-line textarea that shows a character count.

@* MultiLineStringEditor.razor *@
@implements IPropertyComponent

<div class="multiline-editor">
    @if (PropertyContext.GetLabel() is string label)
    {
        <label class="field-label">@label</label>
    }

    <textarea rows="4"
              value="@PropertyContext.Get<string>()"
              @onchange="e => PropertyContext.Set(e.Value)"
              disabled="@PropertyContext.IsReadOnly()"
              maxlength="@MaxLength"
              class="@(PropertyContext.GetShortMessage() is not null ? "field-error" : "")">
    </textarea>

    <div class="char-counter">
        @(PropertyContext.Get<string>()?.Length ?? 0) / @MaxLength
    </div>

    @if (PropertyContext.GetShortMessage() is string error)
    {
        <span class="field-validation-error">@error</span>
    }
</div>

@code {
    [CascadingParameter]
    public PropertyComponentContext PropertyContext { get; set; } = default!;

    [Parameter]
    public int MaxLength { get; set; } = 500;
}

Register to replace the default string edit component globally:

// Program.cs
builder.Services.AddPropertyControlComponents();

builder.Services.AddSingleton(new DynamicComponentRegistration(
    "string",
    DisplayMode.Edit,
    typeof(MultiLineStringEditor)));

Example 2: Custom Enum Display Component (Colored Chips)

This component renders a string-backed status field as a colored chip instead of plain text. It is registered for a specific DataType name so it applies only to status properties, not all string view fields.

@* StatusChipView.razor *@
@implements IPropertyComponent

@{
    var value = PropertyContext.Get<string>();
    var chipClass = value switch
    {
        "Active"   => "chip chip--green",
        "Inactive" => "chip chip--grey",
        "Pending"  => "chip chip--yellow",
        _          => "chip chip--default"
    };
}

<span class="@chipClass">@(value ?? "-")</span>

@code {
    [CascadingParameter]
    public PropertyComponentContext PropertyContext { get; set; } = default!;
}

Register for a custom DataType key named "Status":

// Program.cs
builder.Services.AddSingleton(new DynamicComponentRegistration(
    "Status",
    DisplayMode.View,
    typeof(StatusChipView)));

This registration only activates for properties whose DataType resolves to "Status". Properties with underlying string type but a different DataType continue to use the default StringPropertyView.


Example 3: Registering Both Components in Program.cs

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddPropertyControlComponents();

// Replace the global string edit component with a multi-line textarea.
builder.Services.AddSingleton(new DynamicComponentRegistration(
    "string",
    DisplayMode.Edit,
    typeof(MultiLineStringEditor)));

// Add a colored chip view for the "Status" DataType.
builder.Services.AddSingleton(new DynamicComponentRegistration(
    "Status",
    DisplayMode.View,
    typeof(StatusChipView)));

var app = builder.Build();

Both registrations are singletons. DynamicComponentProvider is also a singleton and receives all registered DynamicComponentRegistration instances through DI constructor injection.


Example 4: One-Off ChildContent Override for a Specific Field

Use ChildContent when a custom appearance is needed for one property on one page and a global registration would be too broad.

@* CustomerDetailPage.razor *@

<DataGraph Builder="@BuildGraph" DataSet="@_dataSet" EntityGuid="@_guid">

    <PropertyComponent Path="Name" />
    <PropertyComponent Path="Email" />

    @* Override only this Notes field with an inline textarea — no global registration needed *@
    <PropertyComponent Path="Notes">
        <div class="notes-field">
            @if (context.GetLabel() is string label)
            {
                <label>@label</label>
            }

            <textarea rows="6"
                      value="@context.Get<string>()"
                      @onchange="e => context.Set(e.Value)"
                      disabled="@context.IsReadOnly()"
                      placeholder="Enter internal notes...">
            </textarea>

            @if (context.GetShortMessage() is string error)
            {
                <span class="field-validation-error">@error</span>
            }
        </div>
    </PropertyComponent>

    <PropertyComponent Path="Status" />

</DataGraph>

The context variable inside ChildContent is a PropertyComponentContext bound to the Notes property. All PropertyComponentContext methods — Get<T>(), Set(), IsReadOnly(), GetLabel(), GetShortMessage() — are available. No IPropertyComponent implementation or DI registration is required.