"EventCallback is Blazor's type-safe event bus. The parent owns the data. The child fires events."
The RFQ form is the most complex UI in this project: a dynamic list of material rows, each with its own inputs and validation, where the parent manages the list and child components handle individual rows. MudBlazor makes this clean with MudForm, MudTextField, and typed EventCallback.
Part of the Green Concrete ERP project. View full project page →
Why MudBlazor Over Plain HTML Forms
Plain HTML forms in Blazor work but require manual validation, manual styling, and manual error message display. MudBlazor gives you MudForm (with Validate() method), MudTextField (with Validation parameter), and a consistent material design that works on mobile. The key advantage: MudForm.Validate() checks all child fields and returns true/false without submitting the form.
MaterialRow Child Component
<!-- MaterialRow.razor -->
<MudTextField @bind-Value="Row.Material" Label="Material" Required="true"
Validation="@((string v) => string.IsNullOrEmpty(v) ? "Required" : null)" />
<MudTextField @bind-Value="Row.QuantityTon" Label="Qty (Ton)" InputType="InputType.Number" />
<MudTextField @bind-Value="Row.OpcPercent" Label="OPC %" InputType="InputType.Number" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error" />
@code {
[Parameter] public MaterialRow Row { get; set; } = new();
[Parameter] public EventCallback<MaterialRow> OnDelete { get; set; }
}Parent Form — Manages the List
<!-- CreateRfq.razor (parent) -->
<MudForm @ref="_form">
@foreach (var row in _rows)
{
<MaterialRowComponent Row="row" />
}
<MudButton>+ Add Material</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary">Submit RFQ</MudButton>
</MudForm>
@code {
private MudForm _form = null!;
private List<MaterialRow> _rows = new() { new() };
private void AddRow() => _rows.Add(new MaterialRow());
private void DeleteRow(MaterialRow row)
{
if (_rows.Count > 1) _rows.Remove(row);
}
private async Task SubmitAsync()
{
await _form.Validate();
if (!_form.IsValid) return;
var cmd = new CreateRfqCommand { Rows = _rows };
await _mediator.Send(cmd);
}
}🎯 Quick Check
Q1: Why does the parent own the _rows list instead of the child managing its own data?
Show Answer
Because the parent needs the complete list to build the CreateRfqCommand on submit. If each child owned its data, the parent would need to collect it from all children — requiring refs to every child component. The parent-owns-data pattern keeps the data flow predictable: data flows down via Parameters, events flow up via EventCallback.
Q2: What is the difference between @bind-Value and Value + ValueChanged in MudBlazor?
Show Answer
@bind-Value is shorthand for Value="row.Property" ValueChanged="v => row.Property = v". They are equivalent. @bind-Value is cleaner for simple two-way binding. Use the explicit Value + ValueChanged form when you need to add custom logic on change (validation, triggering other updates).
Q3: How does await _form.Validate() work in MudBlazor?
Show Answer
MudForm.Validate() calls the Validation function on every field in the form, including fields in child components. It sets IsValid to true only if all validations pass and all required fields are filled. It also shows error messages on all invalid fields. You check IsValid after awaiting Validate() to decide whether to proceed with submission.
Key Takeaways
- MudForm.Validate() checks all child fields and sets IsValid — use it before every submission
- The parent owns the data list; child components display one item and fire EventCallback on changes
- EventCallback<T> is type-safe — passing the wrong type causes a compile error, not a runtime error
- @bind-Value is shorthand for Value + ValueChanged — use explicit form when you need onChange logic
- MaterialRowComponent is reusable — it works in CreateRfq, EditRfq, and ViewRfq without modification