mudneatoo

star 3

This skill should be used when writing or modifying .razor files that bind to Neatoo entities, when using MudBlazor components with Neatoo domain models, when the user mentions "MudNeatooTextField", "MudNeatooSelect", "MudNeatooDatePicker", "MudNeatooNumericField", "MudNeatooCheckBox", "NeatooValidationSummary", "EntityProperty", "@bind-Value with Neatoo", "form binding", "Blazor form", "LazyLoad databinding", "LazyLoad in Razor", "LazyLoad Blazor pattern", "lazy load spinner", or asks to build a form, page, or dialog that edits a Neatoo entity. Also triggers when reviewing .razor files for correct Neatoo integration patterns, including LazyLoad rendering. Assumes the Neatoo skill is also loaded for domain model concepts.

NeatooDotNet By NeatooDotNet schedule Updated 4/14/2026

name: MudNeatoo description: This skill should be used when writing or modifying .razor files that bind to Neatoo entities, when using MudBlazor components with Neatoo domain models, when the user mentions "MudNeatooTextField", "MudNeatooSelect", "MudNeatooDatePicker", "MudNeatooNumericField", "MudNeatooCheckBox", "NeatooValidationSummary", "EntityProperty", "@bind-Value with Neatoo", "form binding", "Blazor form", "LazyLoad databinding", "LazyLoad in Razor", "LazyLoad Blazor pattern", "lazy load spinner", or asks to build a form, page, or dialog that edits a Neatoo entity. Also triggers when reviewing .razor files for correct Neatoo integration patterns, including LazyLoad rendering. Assumes the Neatoo skill is also loaded for domain model concepts. version: 1.0.0

MudNeatoo — Blazor Binding for Neatoo Entities

MudNeatoo provides wrapper components around MudBlazor that bind directly to Neatoo's property system. They handle value synchronization, validation display, busy state, and read-only state automatically. The UI is a thin binding layer over the domain model — no POCOs, no manual event handlers, no duplicate validation.

Required package: Neatoo.Blazor.MudNeatoo

Namespaces & @using Directives

Add these @using directives to your _Imports.razor or individual .razor files:

@using Neatoo.Blazor.MudNeatoo.Components
@using Neatoo.Blazor.MudNeatoo.Validation
@using Neatoo.Blazor.MudNeatoo.Extensions
Namespace Contains
Neatoo.Blazor.MudNeatoo.Components All MudNeatoo input components (MudNeatooTextField, MudNeatooSelect, etc.)
Neatoo.Blazor.MudNeatoo.Validation NeatooValidationSummary
Neatoo.Blazor.MudNeatoo.Extensions EntityPropertyExtensions (GetValidationFunc<T>, GetErrorText, HasErrors)

The Core Pattern

Every MudNeatoo component takes an EntityProperty parameter — the IEntityProperty object accessed via the entity's indexer entity["PropertyName"]. This single binding point gives the component everything: value, label, validation messages, busy state, and read-only state.

@* CORRECT: MudNeatoo component with EntityProperty binding *@
<MudNeatooTextField T="string"
                    EntityProperty="@entity[nameof(IPatient.FirstName)]"
                    Variant="Variant.Outlined" />

The component automatically:

  • Displays DisplayName as the label
  • Calls SetValue() on change (triggers business rules)
  • Shows PropertyMessages as validation errors
  • Disables when IsBusy (async rules running) or when the caller passes Disabled="true"
  • Sets read-only when IsReadOnly

What NOT to Do

@* WRONG: Standard MudBlazor with manual handler *@
<MudTextField T="string"
              Value="@entity.FirstName"
              ValueChanged="OnFirstNameChanged"
              Label="First Name" />

@code {
    void OnFirstNameChanged(string value) {
        entity.FirstName = value;  // Bypasses async rule pipeline
    }
}

@* WRONG: @bind-Value directly to entity property *@
<MudTextField @bind-Value="entity.FirstName" Label="First Name" />

@* WRONG: POCO intermediary *@
<MudTextField @bind-Value="model.FirstName" Label="First Name" />
@code {
    private EditModel model = new();  // Duplicates entity state
    async Task Save() {
        entity.FirstName = model.FirstName;  // Manual sync
    }
}

See references/anti-patterns.md for the complete anti-pattern catalog with real-world examples.

Component Reference

MudNeatoo Component Wraps Type Parameter
MudNeatooTextField<T> MudTextField<T> string, int, etc.
MudNeatooNumericField<T> MudNumericField<T> int, decimal, double
MudNeatooSelect<T> MudSelect<T> Enum or value type
MudNeatooDatePicker MudDatePicker (no type param — always DateTime?)
MudNeatooDateRangePicker MudDateRangePicker (no type param)
MudNeatooTimePicker MudTimePicker (no type param)
MudNeatooCheckBox<T> MudCheckBox<T> bool, bool?
MudNeatooSwitch<T> MudSwitch<T> bool
MudNeatooRadioGroup<T> MudRadioGroup<T> Enum or value type
MudNeatooSlider<T> MudSlider<T> Numeric type
MudNeatooAutocomplete<T> MudAutocomplete<T> Any type
NeatooValidationSummary MudAlert (entity-level errors)

All MudBlazor parameters pass through — Variant, Margin, HelperText, Adornment, Class, Min, Max, etc. — except ReadOnly (hardcoded to EntityProperty.IsReadOnly; see ReadOnly Behavior below) and Disabled (OR'd with EntityProperty.IsBusy; see Disabled Behavior below).

MudNeatooTextField escape hatch: UserAttributes

MudNeatooTextField<T> forwards a UserAttributes (Dictionary<string, object>?) parameter to MudTextField. MudBlazor spreads the dictionary onto the native <input> or <textarea>, so any HTML attribute works — including ones with no typed MudBlazor parameter. Use it for:

  • Spellcheck — MudBlazor does NOT expose a Spellcheck parameter on MudTextField or MudInput. Set ["spellcheck"] = "true" via UserAttributes.
  • Drag-handle resize — CSS resize: vertical on the <textarea>. Set ["style"] = "resize: vertical;" via UserAttributes.
@* Auto-growing textarea with browser spellcheck *@
<MudNeatooTextField T="string"
                    EntityProperty="@entity[nameof(IPatient.Notes)]"
                    Lines="4"
                    Sizing="InputSizing.Auto"
                    MaxLines="20"
                    UserAttributes="@(new() { ["spellcheck"] = "true" })" />

@* Fixed-height textarea with user drag handle *@
<MudNeatooTextField T="string"
                    EntityProperty="@entity[nameof(IPatient.Notes)]"
                    Lines="4"
                    UserAttributes="@(new() { ["style"] = "resize: vertical;" })" />

Multi-line text: Lines, Sizing, MaxLines

MudNeatooTextField<T> also forwards MudBlazor's multi-line parameters:

  • Lines (int, default 1) — set greater than 1 for a <textarea>.
  • Sizing (InputSizing, default Fixed) — InputSizing.Auto grows the textarea with its content (requires Lines > 1).
  • MaxLines (int, default 0) — upper bound on auto-grow. 0 means unlimited.

Binding Patterns

Text and Numeric Fields

<MudNeatooTextField T="string"
                    EntityProperty="@entity[nameof(IPatient.FirstName)]"
                    Variant="Variant.Outlined" />

<MudNeatooNumericField T="decimal"
                       EntityProperty="@entity[nameof(IOrder.UnitPrice)]"
                       Adornment="Adornment.Start"
                       AdornmentText="$" />

Select with Options

<MudNeatooSelect T="PhoneType?"
                 EntityProperty="@phone[nameof(IPersonPhone.PhoneType)]"
                 Placeholder="Select Phone Type">
    <MudSelectItem Value="@((PhoneType?)PhoneType.Home)">Home</MudSelectItem>
    <MudSelectItem Value="@((PhoneType?)PhoneType.Mobile)">Mobile</MudSelectItem>
    <MudSelectItem Value="@((PhoneType?)PhoneType.Work)">Work</MudSelectItem>
</MudNeatooSelect>

Date Picker

<MudNeatooDatePicker EntityProperty="@entity[nameof(IPatient.DateOfBirth)]"
                     MaxDate="@DateTime.Today"
                     DateFormat="MM/dd/yyyy"
                     Editable="true" />

CheckBox

<MudNeatooCheckBox T="bool"
                   EntityProperty="@entity[nameof(IUser.IsActive)]" />

Validation Summary

Display all entity-level validation errors in a MudAlert:

<NeatooValidationSummary Entity="@entity"
                         ShowHeader="false"
                         Dense="true"
                         IncludePropertyNames="false" />

Parameters: Entity (required, IValidateMetaProperties), ShowHeader, HeaderText, Dense, IncludePropertyNames, Variant, Elevation, Class.

Page Structure Pattern

A complete form page follows this structure:

@implements IDisposable
@inject IPatientEditFactory PatientEditFactory

@if (entity != null)
{
    <MudForm @ref="form">
        <NeatooValidationSummary Entity="@entity" ShowHeader="false" Dense="true" />

        <MudNeatooTextField T="string"
                            EntityProperty="@entity[nameof(IPatientEdit.FirstName)]" />
        <MudNeatooTextField T="string"
                            EntityProperty="@entity[nameof(IPatientEdit.LastName)]" />

        <MudButton OnClick="Save"
                   Disabled="@(!entity.IsSavable)"
                   Variant="Variant.Filled"
                   Color="Color.Primary">
            Save
        </MudButton>
    </MudForm>
}

@code {
    private MudForm? form;
    private IPatientEdit? entity;

    protected override async Task OnInitializedAsync()
    {
        entity = await PatientEditFactory.Fetch(patientId);
        if (entity != null)
            entity.PropertyChanged += OnEntityPropertyChanged;
    }

    private async Task Save()
    {
        await entity!.WaitForTasks();
        if (!entity.IsSavable) return;

        var saved = await PatientEditFactory.Save(entity);
        SetEntity(saved);
    }

    private void OnEntityPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        // Re-render when state properties change (IsSavable, IsValid, etc.)
        if (e.PropertyName?.StartsWith("Is") == true)
            InvokeAsync(StateHasChanged);
    }

    private void SetEntity(IPatientEdit? newEntity)
    {
        if (entity != null)
            entity.PropertyChanged -= OnEntityPropertyChanged;
        entity = newEntity;
        if (entity != null)
            entity.PropertyChanged += OnEntityPropertyChanged;
    }

    public void Dispose()
    {
        if (entity != null)
            entity.PropertyChanged -= OnEntityPropertyChanged;
    }
}

Key Points in the Page Structure

  1. Inject the factory interface — not the concrete class
  2. Use IsSavable to disable save button — it combines IsValid && IsModified && !IsBusy && !IsChild
  3. Subscribe to PropertyChanged — so Blazor re-renders when IsSavable changes. This is sufficient here because root meta flags (IsValid, IsModified, IsBusy) bubble through PropertyChanged even when the trigger is a descendant change. For components that need to react to descendant property names (e.g. a custom validation summary), use NeatooPropertyChanged instead. See references/property-change-events.md.
  4. Call WaitForTasks() before save — ensures async validation completes
  5. Replace the entity reference after saveSave() returns the updated entity
  6. Implement IDisposable — unsubscribe from PropertyChanged
  7. No EditForm, no DataAnnotationsValidator — use MudForm with MudNeatoo components

EntityLazyLoad Databinding Pattern

EntityLazyLoad<T> properties use a 4-branch rendering pattern. .Value is a passive read — it never triggers a load. Trigger the load explicitly in OnInitializedAsync(), then bind to .Value and state properties in Razor markup. Blazor re-renders when PropertyChanged fires on load completion.

Trigger the load in OnInitializedAsync():

protected override async Task OnInitializedAsync()
{
    entity = await entityFactory.Fetch(orderId);
    // Explicitly trigger the lazy load — does not block rendering
    _ = entity.OrderLines.LoadAsync();
}

Bind to .Value and state properties in Razor:

@if (entity.OrderLines.HasLoadError)
{
    <MudAlert Severity="Severity.Error">@entity.OrderLines.LoadError</MudAlert>
}
else if (entity.OrderLines.Value is { } orderLines)
{
    <MudButton OnClick="AddLine">Add Line</MudButton>
    @foreach (var line in orderLines)
    {
        <MudNeatooTextField T="string"
                            EntityProperty="@line[nameof(IOrderLine.ProductName)]" />
    }
}
else if (entity.OrderLines.IsLoaded)
{
    <MudAlert Severity="Severity.Warning">No data available</MudAlert>
}
else
{
    <MudProgressCircular Indeterminate="true" Size="Size.Small" />
}

Branch order matters:

  1. HasLoadError — show error state (load failed)
  2. Value != null — show data (loaded successfully with content)
  3. IsLoaded — loaded but null (rare — empty result)
  4. else — loading spinner (load triggered in OnInitializedAsync(), not yet complete)

Accessing the inner collection — use .Value to get the loaded entity:

private void AddLine()
{
    entity!.OrderLines.Value!.AddItem();
}

private Task RemoveLine(IOrderLine line)
{
    return entity!.OrderLines.Value!.RemoveItem(line);
}

Child Entity Collections

Iterate over child collections and bind each child's properties:

@foreach (var item in entity.Items)
{
    <MudGrid Spacing="2">
        <MudItem xs="6">
            <MudNeatooTextField T="string"
                                EntityProperty="@item[nameof(IOrderItem.ProductName)]" />
        </MudItem>
        <MudItem xs="3">
            <MudNeatooNumericField T="int"
                                   EntityProperty="@item[nameof(IOrderItem.Quantity)]" />
        </MudItem>
        <MudItem xs="3">
            <MudIconButton Icon="@Icons.Material.Filled.Delete"
                           OnClick="@(() => entity.Items.Remove(item))" />
        </MudItem>
    </MudGrid>
}

IEntityProperty Metadata

The EntityProperty parameter exposes these metadata properties, all automatically wired by MudNeatoo components:

Property Type What It Does
Value object? Current property value
DisplayName string Label text (from [DisplayName] attribute)
IsValid bool No validation errors on this property
PropertyMessages IReadOnlyCollection<IPropertyMessage> Validation error messages
IsBusy bool Async rules running (shows disabled/spinner)
IsReadOnly bool Property cannot be edited
IsModified bool Property has unsaved changes
SetValue(object?) Task Async value assignment (triggers rules)
WaitForTasks() Task Wait for async rules to complete

Manual Metadata Binding

For custom UI elements not covered by MudNeatoo components, access property metadata directly:

@{ var emailProp = entity["Email"]; }
<MudTextField Value="@((string?)emailProp.Value)"
              ValueChanged="@(async (string v) => await emailProp.SetValue(v))"
              Label="@emailProp.DisplayName"
              Disabled="@emailProp.IsBusy"
              ReadOnly="@emailProp.IsReadOnly" />
@if (!emailProp.IsValid)
{
    @foreach (var msg in emailProp.PropertyMessages)
    { <MudText Color="Color.Error">@msg.Message</MudText> }
}

This is what MudNeatoo components do internally — prefer the components, fall back to manual binding only for unsupported controls.

ReadOnly Behavior

ReadOnly is not a pass-through parameter on MudNeatoo components that wrap a MudBlazor input with a ReadOnly parameter. These components hardcode ReadOnly="@EntityProperty.IsReadOnly" in their Razor templates. You cannot override it via a component parameter.

Exception: MudNeatooSlider does not bind ReadOnly. MudBlazor's MudSlider has no ReadOnly parameter — the slider uses Disabled="@(EntityProperty.IsBusy || Disabled)", following the same Disabled Behavior as all other MudNeatoo components.

IsReadOnly is determined by propertyInfo.IsPrivateSetter:

  • Property with private set or get-only (no setter) -> IsReadOnly = true
  • Property with public setter -> IsReadOnly = false
  • Property with internal set -> IsReadOnly = false (not treated as read-only; PropertyInfoWrapper checks SetMethod?.IsPrivate, and internal setters are not private)

This means read-only state is controlled entirely by the domain model's property declaration. To make a property read-only, declare it with a private set or as get-only.

Disabled Behavior

Every MudNeatoo component accepts an optional Disabled parameter (default false). The inner MudBlazor component is disabled when EITHER EntityProperty.IsBusy is true (async rules running) OR the caller passes Disabled="true". The template expression is:

Disabled="@(EntityProperty.IsBusy || Disabled)"

This means:

  • Without a Disabled parameter, components disable only during async rule execution (backward-compatible default)
  • Passing Disabled="true" (or binding to a condition) forces the field disabled regardless of IsBusy
  • You cannot force a field enabled while IsBusy is true — IsBusy always wins

Common use case — disable fields based on a domain condition:

<MudNeatooTextField T="string"
                    EntityProperty="@entity[nameof(IOrder.ShippingAddress)]"
                    Disabled="@(!entity.RequiresShipping)" />

Contrast with ReadOnly: ReadOnly is hardcoded to EntityProperty.IsReadOnly with no caller override — read-only state is owned entirely by the domain model. Disabled allows both domain-driven (IsBusy) and UI-driven disabling.

Display-Only Binding

For read-only display of entity values (not form inputs), bind directly to entity properties. Blazor does not auto-subscribe to INotifyPropertyChanged — these bindings re-render because the containing form page is already subscribed to entity PropertyChanged for IsSavable (see the Page Structure Pattern above), and display bindings piggyback on that re-render cycle. If you render a view-only page with no form subscription, subscribe explicitly — see references/property-change-events.md.

<MudText>@entity.Total</MudText>
<MudText>@entity.Status</MudText>
<MudChip Color="@(entity.IsValid ? Color.Success : Color.Error)">
    @(entity.IsValid ? "Valid" : "Has Errors")
</MudChip>

View/Edit Mode Pattern

Since most MudNeatoo components bind ReadOnly from IsReadOnly automatically (with no override), the pattern for view/edit mode switching is conditional rendering — not toggling a ReadOnly parameter.

Use plain MudBlazor display components in view mode and MudNeatoo components in edit mode:

@if (isEditing)
{
    <MudNeatooTextField T="string"
                        EntityProperty="@entity[nameof(IPatient.FirstName)]" />
    <MudNeatooTextField T="string"
                        EntityProperty="@entity[nameof(IPatient.LastName)]" />

    <MudButton OnClick="Save" Disabled="@(!entity.IsSavable)">Save</MudButton>
    <MudButton OnClick="CancelEdit" Variant="Variant.Text">Cancel</MudButton>
}
else
{
    <MudText Typo="Typo.body1">@entity.FirstName @entity.LastName</MudText>
    <MudButton OnClick="StartEdit" Variant="Variant.Text">Edit</MudButton>
}

This is intentional — ReadOnly state belongs to the domain model (via private setters), not the UI. View/edit toggling is a UI concern handled with Razor conditionals.

MudBlazor 9.x Compatibility

MudBlazor 9.x renamed ShowMessageBox to ShowMessageBoxAsync on IDialogService. If you use message box dialogs alongside MudNeatoo forms, update your calls accordingly. This is a MudBlazor API change, not a MudNeatoo change.

Reference Documentation

  • references/anti-patterns.md — Complete catalog of anti-patterns with correct alternatives
  • references/property-change-events.mdPropertyChanged vs NeatooPropertyChanged: which to use, why MudNeatoo components only need PropertyChanged, and how to build custom container components that react to deep-graph changes
  • references/aggregate-reactive-vm.md — ViewModel computed properties that stay in sync with live aggregate edits via PropertyChanged subscription
Install via CLI
npx skills add https://github.com/NeatooDotNet/Neatoo --skill mudneatoo
Repository Details
star Stars 3
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
NeatooDotNet
NeatooDotNet Explore all skills →