Blazor Data Binding, Forms, and Validation

This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14.

This section’s bibliography lists the reference material consulted while preparing these pages.

Blazor binds C# state to markup with @bind and validates it with EditForm/EditContext. This page assumes the component model already covered.

One-way and two-way binding

A plain @expression is one-way (state to markup); @bind-Value (or its shorthand @bind) is two-way, listening for a change event and writing the new value back:

<p>Hello, @name</p>                       @* one-way *@
<input @bind="name" />                    @* two-way: writes back on the element's default event (change) *@

@bind:event, @bind:after, @bind:get/@bind:set

<input @bind="name" @bind:event="oninput" />                       @* update on every keystroke *@
<input @bind="name" @bind:after="Search" />                        @* run Search() after the value commits *@
<input @bind:get="Query" @bind:set="OnQueryChanged" />             @* split read/write for custom logic *@
@code {
    private string name = "";
    private string Query { get; set; } = "";

    private void Search() => results = catalog.Search(name);
    private async Task OnQueryChanged(string value)
    {
        Query = value;
        await SaveDraftAsync(value);
    }
}
  • @bind:event overrides the triggering DOM event (default is onchange; oninput fires per keystroke).

  • @bind:after runs a method after the bound value is assigned — convenient for "debounced search" or "recompute a derived value" without manually wiring an event handler.

  • @bind:get / @bind:set split a two-way binding into an explicit getter and setter, useful when the setter needs to be async or must transform the incoming value.

Format strings work the same way as before: <input @bind="startDate" @bind:format="yyyy-MM-dd" />.

EventCallback vs. Action

Prefer EventCallback / EventCallback<T> over a plain Action/Action<T> delegate for parameters a component invokes as an event: EventCallback integrates with Blazor’s rendering so the invoking component re-renders after the callback returns (including awaiting an async handler), which a raw delegate does not do automatically.

@code { [Parameter] public EventCallback<ProductDto> OnBuy { get; set; } }
<button @onclick="() => OnBuy.InvokeAsync(Product)">Buy</button>

EditForm and EditContext

EditForm builds an EditContext over a model and tracks field modification/validation state for its descendants:

<EditForm Model="Input" OnValidSubmit="Save">
    <DataAnnotationsValidator />
    <ValidationSummary />

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

    <InputNumber @bind-Value="Input.Price" />
    <InputDate @bind-Value="Input.ReleaseOn" />
    <InputCheckbox @bind-Value="Input.InStock" />
    <InputSelect @bind-Value="Input.CategoryId">
        @foreach (var c in Categories) { <option value="@c.Id">@c.Name</option> }
    </InputSelect>

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

@code {
    private ProductInput Input = new();
    private Task Save() => ProductApi.CreateAsync(Input);
}

OnValidSubmit (and OnInvalidSubmit) fire only after validation runs; use plain OnSubmit to control that yourself. Injecting EditContext directly (instead of Model) is the way to share one context across nested components or add custom validation logic. See ASP.NET Core Blazor forms overview.

Built-in input components

InputText, InputTextArea, InputNumber, InputSelect, InputCheckbox, InputDate, InputRadio / InputRadioGroup, InputFile, and, since .NET 10, InputHidden — each binds a model field and participates in EditContext validation:

<InputRadioGroup @bind-Value="Input.Size">
    <InputRadio Value="'S'" /> Small
    <InputRadio Value="'M'" /> Medium
    <InputRadio Value="'L'" /> Large
</InputRadioGroup>

<InputFile OnChange="OnFileSelected" multiple />
<InputHidden @bind-Value="Input.CorrelationId" />

Validation

public sealed class ProductInput
{
    [Required, StringLength(100)] public string Name { get; set; } = "";
    [Range(0.01, 100000)] public decimal Price { get; set; }
}

<DataAnnotationsValidator /> wires System.ComponentModel.DataAnnotations attributes into the EditContext; <ValidationSummary /> lists every error, <ValidationMessage For="…​"> shows one field’s error. Add a custom validation attribute (class MustBeEvenAttribute : ValidationAttribute) for reusable rules, or subscribe to EditContext.OnValidationRequested for ad hoc cross-field logic.

NET 10 adds [ValidatableType] for declarative validation of nested objects and collections — previously

DataAnnotationsValidator only validated the top-level model’s own properties:

[ValidatableType]
public sealed class OrderInput
{
    [Required] public string Customer { get; set; } = "";
    public List<OrderLineInput> Lines { get; set; } = [];
}

public sealed class OrderLineInput
{
    [Required] public string Sku { get; set; } = "";
    [Range(1, 100)] public int Quantity { get; set; }
}

Marking OrderInput (and transitively validated types) [ValidatableType] makes DataAnnotationsValidator walk into Lines and validate each OrderLineInput, which previously required a hand-written IValidatableObject implementation. See ASP.NET Core Blazor forms validation.

SSR form handling

A Static SSR page has no interactive circuit to bind against, so its EditForm posts a normal HTTP form submission; recover the values with [SupplyParameterFromForm] and protect the endpoint with antiforgery:

@page "/products/create"
@attribute [RenderModeStatic]

<EditForm Model="Input" method="post" OnValidSubmit="Save" FormName="create-product">
    <AntiforgeryToken />
    <DataAnnotationsValidator />
    <InputText @bind-Value="Input.Name" />
    <button type="submit">Save</button>
</EditForm>

@code {
    [SupplyParameterFromForm] private ProductInput Input { get; set; } = new();

    private async Task Save()
    {
        await ProductApi.CreateAsync(Input);
        Nav.NavigateTo("/products");
    }
}

Antiforgery validation is enabled for Blazor Web App form posts by default via AddAntiforgery and UseAntiforgery; <AntiforgeryToken /> embeds the token in the rendered form. See ASP.NET Core Blazor forms overview and Security Hardening for antiforgery/CSRF in general.