Updated Jun 23, 2026

Forms & Validation

Sooner or later your products UI needs a real form — a screen where someone types a product name, a price, and hits Save. And the moment you have a form, you have a second job: stopping bad data before it reaches your database. Empty names. Negative prices. A description that's three paragraphs longer than your column allows.

You could wire all that by hand — track every input, check every value on submit, render every error message yourself. People did, for years. Blazor gives you a system instead, and once you see the shape of it, you won't want to go back.

The mental model: a form is a model with a guard

Here's the one idea to hold. In Blazor, a form is built around a model object — a plain C# class that holds the data being edited. The <EditForm> component wraps that model and quietly tracks its state: which fields changed, which are valid, whether the whole thing is ready to submit. That tracking object is called the EditContext, and Blazor creates it for you the instant you hand EditForm a model.

Three moving parts, and they all point at the same model:

  1. The model — a class with your fields (Name, Price) plus annotations that describe the rules ([Required], [Range]).
  2. The inputs — components like InputText that bind to model properties, so typing updates the model and the model updates the screen.
  3. The validator — a component that reads the model's annotations and decides, field by field, whether the data is valid.

💡 The shape to keep in your head: inputs write to the model, the validator checks the model, the form submits the model. Everything in this phase is one of those three roles.

Let's build it up piece by piece, using a product form.

EditForm and the input components

Start with the container. EditForm needs a Model — the object it's editing — and a handler for when the form is submitted successfully.

<EditForm Model="@product" OnValidSubmit="Save">
    <label>
        Name
        <InputText @bind-Value="product.Name" />
    </label>

    <label>
        Price
        <InputNumber @bind-Value="product.Price" />
    </label>

    <button type="submit">Save</button>
</EditForm>

@code {
    private ProductForm product = new();

    private void Save()
    {
        // product.Name and product.Price are filled in from the inputs.
    }
}

public class ProductForm
{
    public string Name { get; set; } = "";
    public decimal Price { get; set; }
}

What just happened: EditForm rendered a real HTML <form> and built an EditContext around product. Each input is a built-in Blazor component — not a raw <input> — and @bind-Value="product.Name" is two-way binding (the same @bind idea from Phase 3, here named Value because that's the input's parameter). Type in the box, product.Name updates; change product.Name in code, the box updates. When you click Save, OnValidSubmit runs your Save method with the model already populated.

Blazor ships an input component for each common field type. You bind every one of them with @bind-Value:

Component For
InputText single-line text
InputTextArea multi-line text
InputNumber numeric types (int, decimal, …)
InputSelect dropdowns (<option> children)
InputCheckbox booleans
InputDate dates (DateTime, DateOnly)
InputRadioGroup a set of radio buttons

📝 Why use these instead of plain <input> tags? Because the built-in components plug into the EditContext. They report changes back to it, and — crucially for the next section — they know how to show themselves as invalid. A raw <input> is just HTML; it's outside the form's awareness.

Validation: annotations plus a validator

Right now nothing stops a user from saving an empty name or a price of negative ten. Let's add rules.

Rules live as data annotation attributes on the model. They're declarative — you describe the constraint, not the checking code:

using System.ComponentModel.DataAnnotations;

public class ProductForm
{
    [Required(ErrorMessage = "A product needs a name.")]
    [StringLength(120)]
    public string Name { get; set; } = "";

    [Range(0, 100000, ErrorMessage = "Price must be between 0 and 100,000.")]
    public decimal Price { get; set; }
}

What just happened: nothing yet, actually — and that's the whole catch. [Required], [StringLength], and [Range] are passive metadata. They sit on the class doing absolutely nothing until something reads them. Common annotations include [Required], [StringLength(max)], [Range(min, max)], and [EmailAddress].

⚠️ The annotations alone do nothing. They are inert until you place a <DataAnnotationsValidator /> inside the EditForm. This is the single most common Blazor forms mistake — a form that "ignores" its rules almost always means the validator component is missing. The attributes describe the rules; the component enforces them.

So you add the validator, plus a way to show the errors. <ValidationSummary /> lists every error at once; <ValidationMessage For="..." /> shows the error for one specific field, right next to it. Here's the full, working form:

@using System.ComponentModel.DataAnnotations

<EditForm Model="@product" OnValidSubmit="Save">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <label>
        Name
        <InputText @bind-Value="product.Name" />
        <ValidationMessage For="@(() => product.Name)" />
    </label>

    <label>
        Price
        <InputNumber @bind-Value="product.Price" />
        <ValidationMessage For="@(() => product.Price)" />
    </label>

    <button type="submit">Save</button>
</EditForm>

@code {
    private ProductForm product = new();

    private void Save()
    {
        // We only get here if every annotation passed.
    }
}

public class ProductForm
{
    [Required(ErrorMessage = "A product needs a name.")]
    [StringLength(120)]
    public string Name { get; set; } = "";

    [Range(0, 100000)]
    public decimal Price { get; set; }
}

What just happened: <DataAnnotationsValidator /> hooked into the EditContext and started reading the model's annotations. Now, when the user edits a field or tries to submit, it validates against the rules and marks fields valid or invalid. <ValidationSummary /> renders the full error list near the top; each <ValidationMessage For="@(() => product.Name)" /> renders just that field's error inline. The For argument is a little lambda that points at the property — that's how the message component knows which field it represents.

💡 Look closely at ProductForm: it's a plain class with [Required] and [Range]. Those are the exact same System.ComponentModel.DataAnnotations attributes you'd put on an ASP.NET Core DTO or an EF Core entity. The knowledge transfers directly — if you've validated a request body in a Web API, you already know how to validate a Blazor form.

Submit events: OnValidSubmit vs OnSubmit

EditForm gives you three submit events. Pick based on who does the validating.

  • OnValidSubmit — fires only when validation passes. This is the one you'll reach for most: by the time your handler runs, the model is guaranteed valid, so you can save straight away. (Pair it with OnInvalidSubmit if you want to react to failed attempts — say, scroll to the first error.)
  • OnSubmit — fires on every submit, valid or not. Choosing this means you're taking the wheel: Blazor won't gate the handler, so you call the validation yourself and decide what to do.

You almost always want the first. Here's the contrast:

@* Clean path: handler only runs on valid data *@
<EditForm Model="@product" OnValidSubmit="Save" OnInvalidSubmit="ShowProblems">
    <DataAnnotationsValidator />
    <!-- inputs… -->
    <button type="submit">Save</button>
</EditForm>

@* Manual path: you validate, you decide *@
<EditForm Model="@product" OnSubmit="HandleEverything">
    <DataAnnotationsValidator />
    <!-- inputs… -->
    <button type="submit">Save</button>
</EditForm>

@code {
    private ProductForm product = new();

    private void Save() { /* product is valid — persist it */ }
    private void ShowProblems() { /* at least one field failed */ }

    private void HandleEverything(EditContext ctx)
    {
        if (ctx.Validate())
        {
            // valid — go ahead
        }
        // ctx tells you the state; you choose what happens next
    }
}

What just happened: the first form splits the two outcomes cleanly — Save for success, ShowProblems for failure — and you never write an if to check validity. The second form routes everything through one handler that receives the EditContext; you call ctx.Validate() yourself and branch on the result. Use OnSubmit only when you need that manual control; for ordinary save-this-product forms, OnValidSubmit is less code and harder to get wrong.

💡 Data annotations are perfect for field-level rules. When you need richer logic — "this field is required only if that other one is set," or cross-field comparisons — reach for FluentValidation, which has community Blazor integrations that slot into the same EditContext. Start with annotations; graduate to FluentValidation when the rules outgrow attributes.

Recap

  • A Blazor form is built around a model object; <EditForm Model="@model"> wraps it and tracks its state in an EditContext.
  • Use the built-in input components (InputText, InputNumber, InputSelect, …) and bind them with @bind-Value so they participate in the form's validation.
  • Validation rules are data annotations ([Required], [StringLength], [Range], [EmailAddress]) on the model — the same attributes you'd use on an ASP.NET Core DTO.
  • Annotations do nothing on their own: you must add <DataAnnotationsValidator /> inside the form, then show errors with <ValidationSummary /> and <ValidationMessage For="@(() => model.Field)" />.
  • Prefer OnValidSubmit (runs only when valid); use OnSubmit when you want to call EditContext.Validate() and handle outcomes manually. For complex rules, consider FluentValidation.

Quick check

[
  {
    "q": "You added [Required] and [Range] to your model, but the form saves invalid data anyway. What's the most likely cause?",
    "choices": ["The model class is in the wrong namespace", "There's no <DataAnnotationsValidator /> inside the EditForm", "You used InputText instead of a plain <input>", "OnValidSubmit doesn't support validation"],
    "answer": 1,
    "explain": "Data annotations are inert metadata. Nothing checks them until a <DataAnnotationsValidator /> component is placed inside the EditForm."
  },
  {
    "q": "How do you bind a built-in input component to a model property?",
    "choices": ["value=\"product.Name\"", "@bind=\"product.Name\"", "@bind-Value=\"product.Name\"", "Model=\"product.Name\""],
    "answer": 2,
    "explain": "Built-in input components expose a Value parameter, so two-way binding uses @bind-Value=\"product.Name\"."
  },
  {
    "q": "You want a handler that runs only when the form's data passes validation. Which event do you use?",
    "choices": ["OnSubmit", "OnInvalidSubmit", "OnValidSubmit", "OnChange"],
    "answer": 2,
    "explain": "OnValidSubmit fires only after validation succeeds, so the model is guaranteed valid inside the handler — no manual check needed."
  }
]

← Phase 4: Events & the Component Lifecycle · Guide overview · Phase 6: Component Communication & State →

Check your understanding

1. You added [Required] and [Range] to your model, but the form saves invalid data anyway. What's the most likely cause?

2. How do you bind a built-in input component to a model property?

3. You want a handler that runs only when the form's data passes validation. Which event do you use?

Was this page helpful?