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
DisplayNameas the label - Calls
SetValue()on change (triggers business rules) - Shows
PropertyMessagesas validation errors - Disables when
IsBusy(async rules running) or when the caller passesDisabled="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
Spellcheckparameter onMudTextFieldorMudInput. Set["spellcheck"] = "true"viaUserAttributes. - Drag-handle resize — CSS
resize: verticalon the<textarea>. Set["style"] = "resize: vertical;"viaUserAttributes.
@* 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, default1) — set greater than1for a<textarea>.Sizing(InputSizing, defaultFixed) —InputSizing.Autogrows the textarea with its content (requiresLines > 1).MaxLines(int, default0) — upper bound on auto-grow.0means 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
- Inject the factory interface — not the concrete class
- Use
IsSavableto disable save button — it combinesIsValid && IsModified && !IsBusy && !IsChild - Subscribe to
PropertyChanged— so Blazor re-renders whenIsSavablechanges. This is sufficient here because root meta flags (IsValid,IsModified,IsBusy) bubble throughPropertyChangedeven when the trigger is a descendant change. For components that need to react to descendant property names (e.g. a custom validation summary), useNeatooPropertyChangedinstead. Seereferences/property-change-events.md. - Call
WaitForTasks()before save — ensures async validation completes - Replace the entity reference after save —
Save()returns the updated entity - Implement
IDisposable— unsubscribe fromPropertyChanged - No
EditForm, noDataAnnotationsValidator— useMudFormwith 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:
- HasLoadError — show error state (load failed)
- Value != null — show data (loaded successfully with content)
- IsLoaded — loaded but null (rare — empty result)
- 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 setor get-only (no setter) ->IsReadOnly = true - Property with public setter ->
IsReadOnly = false - Property with
internal set->IsReadOnly = false(not treated as read-only;PropertyInfoWrapperchecksSetMethod?.IsPrivate, andinternalsetters 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
Disabledparameter, components disable only during async rule execution (backward-compatible default) - Passing
Disabled="true"(or binding to a condition) forces the field disabled regardless ofIsBusy - You cannot force a field enabled while
IsBusyis true —IsBusyalways 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 alternativesreferences/property-change-events.md—PropertyChangedvsNeatooPropertyChanged: which to use, why MudNeatoo components only needPropertyChanged, and how to build custom container components that react to deep-graph changesreferences/aggregate-reactive-vm.md— ViewModel computed properties that stay in sync with live aggregate edits via PropertyChanged subscription