Forms and Validation
EditForm binds a form to a model object, validates it with data annotations, and submits it to a handler method.
<EditForm Model="Model" FormName="Name" OnValidSubmit="Handler">EditForm renders an HTML <form> and attaches it to a C# object you supply as its Model. It builds an EditContext around that object, which tracks which fields the user has touched and what is currently wrong with them. The input components and the validation messages all read that context.
Inputs and messages
Use the built-in inputs rather than raw HTML elements: InputText, InputTextArea, InputNumber<TValue>, InputCheckbox, InputDate<TValue>, InputSelect<TValue>, InputRadioGroup<TValue> with InputRadio<TValue>, InputFile, and InputHidden (added in .NET 10). Each one binds with @bind-Value and reports its own validity. Rules live on the model as data annotations, attributes such as [Required] and [Range]. Add <DataAnnotationsValidator /> inside the form to make them run, then show the failures with <ValidationSummary /> for all of them or <ValidationMessage For="@(() => Model.Email)" /> next to one field.
Submitting
OnValidSubmit runs only when validation passed. OnInvalidSubmit runs only when it failed. OnSubmit runs either way and leaves validation to you. Pick one of the three, not OnSubmit together with another.
What trips people up
With no render mode the form is a real HTTP POST, so the component is rebuilt from scratch. Mark the model property [SupplyParameterFromForm] and Blazor fills it from the posted data; without that the model comes back empty.
Example
* Components/Pages/SignUp.razor *
"/signup"
System.ComponentModel.DataAnnotations
* No render mode, so submitting this form is an ordinary HTTP POST to /signup. *
<EditForm Model="Model" FormName="SignUp" OnValidSubmit="Save">
<DataAnnotationsValidator />
<label>
Email:
<InputText -Value="Model!.Email" />
</label>
<ValidationMessage For="@(() => Model!.Email)" />
<label>
Seats:
<InputNumber -Value="Model!.Seats" />
</label>
<button type="submit">Save</button>
</EditForm>
(_saved)
{
<p>Saved !.Email for !.Seats seats.</p>
}
{
private bool _saved;
// Filled from the posted form data. [SupplyParameterFromForm] properties
// may be private, unlike [Parameter] ones.
[SupplyParameterFromForm]
private Registration? Model { get; set; }
// Runs on the first render and on every POST, so only fill in a gap.
protected override void OnInitialized() => Model ??= new();
private void Save() => _saved = true;
public class Registration
{
[Required]
[EmailAddress(ErrorMessage = "Enter a valid email address.")]
public string? Email { get; set; }
[Range(1, 10, ErrorMessage = "Choose 1 to 10 seats.")]
public int Seats { get; set; } = 1;
}
}When to use it
- Collect and validate a new customer record without writing any client-side validation code.
- Show a server-side error, such as an email address already in use, beside the field that caused it.
- Keep a long form working with JavaScript disabled by leaving it statically rendered.
More examples
The same form, interactive, saving through a service
In an interactive component no page post happens, so the model is a plain field and [SupplyParameterFromForm] is unnecessary. FormName is still worth setting: if interactivity is ever removed from the page, the form keeps working instead of throwing on POST. Disabling the button while saving is the usual fix for duplicate records.
@* Components/Pages/SignUpLive.razor *@
@page "/signup-live"
@rendermode InteractiveServer
@inject RegistrationStore Store @* @inject asks the container for a service *@
<EditForm Model="_model" FormName="SignUpLive" OnValidSubmit="SaveAsync">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText @bind-Value="_model.Email" />
@* Disabled while the save is in flight, so one click means one record. *@
<button type="submit" disabled="@_saving">Save</button>
</EditForm>
@code {
private readonly Registration _model = new();
private bool _saving;
// OnValidSubmit accepts an async handler; the form awaits it.
private async Task SaveAsync()
{
_saving = true;
await Store.AddAsync(_model);
_saving = false;
}
}Adding an error the model could not know about
Data annotations can only check the shape of a value, not whether a database already holds it. A ValidationMessageStore lets server-side answers appear in the same place as the annotation errors. Clear the store at the start of each submit, or yesterday's message stays on screen.
@* Components/Pages/ClaimName.razor *@
@page "/claim-name"
@rendermode InteractiveServer
@inject NameRegistry Registry
<EditForm EditContext="_editContext" FormName="ClaimName" OnValidSubmit="SubmitAsync">
<DataAnnotationsValidator />
<InputText @bind-Value="_model.Name" />
<ValidationMessage For="@(() => _model.Name)" />
<button type="submit">Claim</button>
</EditForm>
@code {
private readonly Claim _model = new();
private EditContext? _editContext;
private ValidationMessageStore? _messages;
protected override void OnInitialized()
{
// Pass EditContext instead of Model when you need the context yourself.
// Never pass both: that throws at runtime.
_editContext = new EditContext(_model);
_messages = new ValidationMessageStore(_editContext);
}
private async Task SubmitAsync()
{
_messages!.Clear();
if (await Registry.IsTakenAsync(_model.Name!))
{
// Attach the message to one field, then tell the form to repaint.
_messages.Add(_editContext!.Field(nameof(Claim.Name)), "Already taken.");
_editContext.NotifyValidationStateChanged();
}
}
}A select bound to an enum, with radio buttons
InputSelect and InputRadio are generic over the bound type, so an enum works with no conversion code and an unparseable value becomes a validation error rather than an exception. Radio buttons must sit inside an InputRadioGroup; on their own they are not grouped and the browser lets the user select more than one.
@* Components/Pages/Shipping.razor *@
@page "/shipping"
@rendermode InteractiveServer
<EditForm Model="_model" FormName="Shipping" OnValidSubmit="Submit">
<DataAnnotationsValidator />
@* TValue is inferred from the bound property, here Speed. *@
<InputSelect @bind-Value="_model.Speed">
@foreach (var speed in Enum.GetValues<Speed>())
{
<option value="@speed">@speed</option>
}
</InputSelect>
<InputRadioGroup @bind-Value="_model.Insured">
<label><InputRadio Value="true" /> Insured</label>
<label><InputRadio Value="false" /> Not insured</label>
</InputRadioGroup>
<button type="submit">Continue</button>
</EditForm>
@code {
private readonly Order _model = new();
private void Submit() { }
public enum Speed { Standard, Express, Overnight }
public class Order
{
public Speed Speed { get; set; } = Speed.Standard;
public bool Insured { get; set; }
}
}
Discussion