ASP.NET Core Blazor forms and validation

By Daniel Roth, Rémi Bourgarel, and Luke Latham

Forms and validation are supported in Blazor using data annotations.

The following ExampleModel type defines validation logic using data annotations:

using System.ComponentModel.DataAnnotations;

public class ExampleModel
{
    [Required]
    [StringLength(10, ErrorMessage = "Name is too long.")]
    public string Name { get; set; }
}

A form is defined using the EditForm component. The following form demonstrates typical elements, components, and Razor code:

<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <InputText id="name" @bind-Value="exampleModel.Name" />

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

@code {
    private ExampleModel exampleModel = new ExampleModel();

    private void HandleValidSubmit()
    {
        ...
    }
}

In the preceding example:

  • The form validates user input in the name field using the validation defined in the ExampleModel type. The model is created in the component's @code block and held in a private field (exampleModel). The field is assigned to the Model attribute of the <EditForm> element.
  • The InputText component's @bind-Value binds:
  • The DataAnnotationsValidator validator component attaches validation support using data annotations.
  • The ValidationSummary component summarizes validation messages.
  • HandleValidSubmit is triggered when the form successfully submits (passes validation).

Built-in forms components

A set of built-in components are available to receive and validate user input. Inputs are validated when they're changed and when a form is submitted. Available input components are shown in the following table.

Input component Rendered as…
InputCheckbox <input type="checkbox">
InputDate<TValue> <input type="date">
InputFile <input type="file">
InputNumber<TValue> <input type="number">
InputRadio <input type="radio">
InputRadioGroup <input type="radio">
InputSelect<TValue> <select>
InputText <input>
InputTextArea <textarea>
Input component Rendered as…
InputCheckbox <input type="checkbox">
InputDate<TValue> <input type="date">
InputNumber<TValue> <input type="number">
InputSelect<TValue> <select>
InputText <input>
InputTextArea <textarea>

Note

The InputRadio and InputRadioGroup components are available in ASP.NET Core 5.0 or later. For more information, select a 5.0 or later version of this article.

All of the input components, including EditForm, support arbitrary attributes. Any attribute that doesn't match a component parameter is added to the rendered HTML element.

Input components provide default behavior for validating when a field is changed, including updating the field CSS class to reflect the field state. Some components include useful parsing logic. For example, InputDate<TValue> and InputNumber<TValue> handle unparseable values gracefully by registering unparseable values as validation errors. Types that can accept null values also support nullability of the target field (for example, int?).

The following Starship type defines validation logic using a larger set of properties and data annotations than the earlier ExampleModel:

using System;
using System.ComponentModel.DataAnnotations;

public class Starship
{
    [Required]
    [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")]
    public string Identifier { get; set; }

    public string Description { get; set; }

    [Required]
    public string Classification { get; set; }

    [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]
    public int MaximumAccommodation { get; set; }

    [Required]
    [Range(typeof(bool), "true", "true", 
        ErrorMessage = "This form disallows unapproved ships.")]
    public bool IsValidatedDesign { get; set; }

    [Required]
    public DateTime ProductionDate { get; set; }
}

In the preceding example, Description is optional because no data annotations are present.

The following form validates user input using the validation defined in the Starship model:

@page "/FormsValidation"

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>
    <p>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="starship.Description" />
        </label>
    </p>
    <p>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="starship.Classification">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </p>
    <p>
        <label>
            Maximum Accommodation:
            <InputNumber @bind-Value="starship.MaximumAccommodation" />
        </label>
    </p>
    <p>
        <label>
            Engineering Approval:
            <InputCheckbox @bind-Value="starship.IsValidatedDesign" />
        </label>
    </p>
    <p>
        <label>
            Production Date:
            <InputDate @bind-Value="starship.ProductionDate" />
        </label>
    </p>

    <button type="submit">Submit</button>

    <p>
        <a href="http://www.startrek.com/">Star Trek</a>, 
        &copy;1966-2019 CBS Studios, Inc. and 
        <a href="https://www.paramount.com">Paramount Pictures</a>
    </p>
</EditForm>

@code {
    private Starship starship = new Starship() { ProductionDate = DateTime.UtcNow };

    private void HandleValidSubmit()
    {
        ...
    }
}

The EditForm creates an EditContext as a cascading value that tracks metadata about the edit process, including which fields have been modified and the current validation messages.

Assign either an EditContext or an EditForm.Model to an EditForm. Assignment of both isn't supported and generates a runtime error.

The EditForm provides convenient events for valid and invalid form submission:

Use OnSubmit to use custom code to trigger validation and check field values.

In the following example:

  • The HandleSubmit method executes when the Submit button is selected.
  • The form is validated by calling EditContext.Validate.
  • Additional code is executed depending on the validation result. Place business logic in the method assigned to OnSubmit.
<EditForm EditContext="@editContext" OnSubmit="@HandleSubmit">

    ...

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

@code {
    private Starship starship = new Starship() { ProductionDate = DateTime.UtcNow };
    private EditContext editContext;

    protected override void OnInitialized()
    {
        editContext = new EditContext(starship);
    }

    private async Task HandleSubmit()
    {
        var isValid = editContext.Validate();

        if (isValid)
        {
            ...
        }
        else
        {
            ...
        }
    }
}

Note

Framework API doesn't exist to clear validation messages directly from an EditContext. Therefore, we don't generally recommend adding validation messages to a new ValidationMessageStore in a form. To manage validation messages, use a validator component with your business logic validation code, as described in this article.

Display name support

This section applies to ASP.NET Core in .NET 5 Release Candidate 1 (RC1) or later.

The following built-in components support display names with the DisplayName parameter:

In the following InputDate component example:

  • The display name (DisplayName) is set to birthday.
  • The component is bound to the BirthDate property as a DateTime type.
<InputDate @bind-Value="@BirthDate" DisplayName="birthday" />

@code {
    public DateTime BirthDate { get; set; }
}

If the user doesn't provide a date value, the validation error appears as:

The birthday must be a date.

Validator components

Validator components support form validation by managing a ValidationMessageStore for a form's EditContext.

The Blazor framework provides the DataAnnotationsValidator component to attach validation support to forms based on validation attributes (data annotations). Create custom validator components to process validation messages for different forms on the same page or the same form at different steps of form processing, for example client-side validation followed by server-side validation. The validator component example shown in this section, CustomValidator, is used in the following sections of this article:

Note

Custom data annotation validation attributes can be used instead of custom validator components in many cases. Custom attributes applied to the form's model activate with the use of the DataAnnotationsValidator component. When used with server-side validation, any custom attributes applied to the model must be executable on the server. For more information, see Model validation in ASP.NET Core MVC.

Create a validator component from ComponentBase:

  • The form's EditContext is a cascading parameter of the component.
  • When the validator component is initialized, a new ValidationMessageStore is created to maintain a current list of form errors.
  • The message store receives errors when developer code in the form's component calls the DisplayErrors method. The errors are passed to the DisplayErrors method in a Dictionary<string, List<string>>. In the dictionary, the key is the name of the form field that has one or more errors. The value is the error list.
  • Messages are cleared when any of the following have occurred:
    • Validation is requested on the EditContext when the OnValidationRequested event is raised. All of the errors are cleared.
    • A field changes in the form when the OnFieldChanged event is raised. Only the errors for the field are cleared.
    • The ClearErrors method is called by developer code. All of the errors are cleared.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample.Client
{
    public class CustomValidator : ComponentBase
    {
        private ValidationMessageStore messageStore;

        [CascadingParameter]
        private EditContext CurrentEditContext { get; set; }

        protected override void OnInitialized()
        {
            if (CurrentEditContext == null)
            {
                throw new InvalidOperationException(
                    $"{nameof(CustomValidator)} requires a cascading " +
                    $"parameter of type {nameof(EditContext)}. " +
                    $"For example, you can use {nameof(CustomValidator)} " +
                    $"inside an {nameof(EditForm)}.");
            }

            messageStore = new ValidationMessageStore(CurrentEditContext);

            CurrentEditContext.OnValidationRequested += (s, e) => 
                messageStore.Clear();
            CurrentEditContext.OnFieldChanged += (s, e) => 
                messageStore.Clear(e.FieldIdentifier);
        }

        public void DisplayErrors(Dictionary<string, List<string>> errors)
        {
            foreach (var err in errors)
            {
                messageStore.Add(CurrentEditContext.Field(err.Key), err.Value);
            }

            CurrentEditContext.NotifyValidationStateChanged();
        }

        public void ClearErrors()
        {
            messageStore.Clear();
            CurrentEditContext.NotifyValidationStateChanged();
        }
    }
}

Business logic validation

Business logic validation can be accomplished with a validator component that receives form errors in a dictionary.

In the following example:

  • The CustomValidator component from the Validator components section of this article is used.
  • The validation requires a value for the ship's description (Description) if the user selects the Defense ship classification (Classification).

When validation messages are set in the component, they're added to the validator's ValidationMessageStore and shown in the EditForm:

@page "/FormsValidation"

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <CustomValidator @ref="customValidator" />
    <ValidationSummary />

    ...

</EditForm>

@code {
    private CustomValidator customValidator;
    private Starship starship = new Starship() { ProductionDate = DateTime.UtcNow };

    private void HandleValidSubmit()
    {
        customValidator.ClearErrors();

        var errors = new Dictionary<string, List<string>>();

        if (starship.Classification == "Defense" &&
                string.IsNullOrEmpty(starship.Description))
        {
            errors.Add(nameof(starship.Description),
                new List<string>() { "For a 'Defense' ship classification, " +
                "'Description' is required." });
        }

        if (errors.Count() > 0)
        {
            customValidator.DisplayErrors(errors);
        }
        else
        {
            // Process the form
        }
    }
}

Note

As an alternative to using validation components, data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the DataAnnotationsValidator component. When used with server-side validation, the attributes must be executable on the server. For more information, see Model validation in ASP.NET Core MVC.

Server validation

Server validation can be accomplished with a server validator component:

  • Process client-side validation in the form with the DataAnnotationsValidator component.
  • When the form passes client-side validation (OnValidSubmit is called), send the EditContext.Model to a backend server API for form processing.
  • Process model validation on the server.
  • The server API includes both the built-in framework data annotations validation and custom validation logic supplied by the developer. If validation passes on the server, process the form and send back a success status code (200 - OK). If validation fails, return a failure status code (400 - Bad Request) and the field validation errors.
  • Either disable the form on success or display the errors.

The following example is based on:

In the following example, the server API validates that a value is provided for the ship's description (Description) if the user selects the Defense ship classification (Classification).

Place the Starship model into the solution's Shared project so that both the client and server apps can use the model. Since the model requires data annotations, add a package reference for System.ComponentModel.Annotations to the Shared project's project file:

<ItemGroup>
  <PackageReference Include="System.ComponentModel.Annotations" Version="{VERSION}" />
</ItemGroup>

To determine the latest non-preview version of the package, see the package Version History at NuGet.org.

In the server API project, add a controller to process starship validation requests (Controllers/StarshipValidation.cs) and return failed validation messages:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using BlazorSample.Shared;

namespace BlazorSample.Server.Controllers
{
    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class StarshipValidationController : ControllerBase
    {
        private readonly ILogger<StarshipValidationController> logger;

        public StarshipValidationController(
            ILogger<StarshipValidationController> logger)
        {
            this.logger = logger;
        }

        [HttpPost]
        public async Task<IActionResult> Post(Starship starship)
        {
            try
            {
                if (starship.Classification == "Defense" && 
                    string.IsNullOrEmpty(starship.Description))
                {
                    ModelState.AddModelError(nameof(starship.Description),
                        "For a 'Defense' ship " +
                        "classification, 'Description' is required.");
                }
                else
                {
                    // Process the form asynchronously
                    // async ...

                    return Ok(ModelState);
                }
            }
            catch (Exception ex)
            {
                logger.LogError("Validation Error: {MESSAGE}", ex.Message);
            }

            return BadRequest(ModelState);
        }
    }
}

When a model binding validation error occurs on the server, an ApiController (ApiControllerAttribute) normally returns a default bad request response with a ValidationProblemDetails. The response contains more data than just the validation errors, as shown in the following example when all of the fields of the Starfleet Starship Database form aren't submitted and the form fails validation:

{
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Identifier": ["The Identifier field is required."],
    "Classification": ["The Classification field is required."],
    "IsValidatedDesign": ["This form disallows unapproved ships."],
    "MaximumAccommodation": ["Accommodation invalid (1-100000)."]
  }
}

If the server API returns the preceding default JSON response, it's possible for the client to parse the response to obtain the children of the errors node. However, it's inconvenient to parse the file. Parsing the JSON requires additional code after calling ReadFromJsonAsync in order to produce a Dictionary<string, List<string>> of errors for forms validation error processing. Ideally, the server API should only return the validation errors:

{
  "Identifier": ["The Identifier field is required."],
  "Classification": ["The Classification field is required."],
  "IsValidatedDesign": ["This form disallows unapproved ships."],
  "MaximumAccommodation": ["Accommodation invalid (1-100000)."]
}

To modify the server API's response to make it only return the validation errors, change the delegate that's invoked on actions that are annotated with ApiControllerAttribute in Startup.ConfigureServices. For the API endpoint (/StarshipValidation), return a BadRequestObjectResult with the ModelStateDictionary. For any other API endpoints, preserve the default behavior by returning the object result with a new ValidationProblemDetails:

using Microsoft.AspNetCore.Mvc;

...

services.AddControllersWithViews()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            if (context.HttpContext.Request.Path == "/StarshipValidation")
            {
                return new BadRequestObjectResult(context.ModelState);
            }
            else
            {
                return new BadRequestObjectResult(
                    new ValidationProblemDetails(context.ModelState));
            }
        };
    });

For more information, see Handle errors in ASP.NET Core web APIs.

In the client project, add the validator component shown in the Validator components section.

In the client project, the Starfleet Starship Database form is updated to show server validation errors with help of the CustomValidator component. When the server API returns validation messages, they're added to the CustomValidator component's ValidationMessageStore. The errors are available in the form's EditContext for display by the form's ValidationSummary:

@page "/FormValidation"
@using System.Net
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.Extensions.Logging
@using BlazorSample.Shared
@attribute [Authorize]
@inject HttpClient Http
@inject ILogger<FormValidation> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <CustomValidator @ref="customValidator" />
    <ValidationSummary />

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" disabled="@disabled" />
        </label>
    </p>
    <p>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="starship.Description" 
                disabled="@disabled" />
        </label>
    </p>
    <p>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="starship.Classification" disabled="@disabled">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </p>
    <p>
        <label>
            Maximum Accommodation:
            <InputNumber @bind-Value="starship.MaximumAccommodation" 
                disabled="@disabled" />
        </label>
    </p>
    <p>
        <label>
            Engineering Approval:
            <InputCheckbox @bind-Value="starship.IsValidatedDesign" 
                disabled="@disabled" />
        </label>
    </p>
    <p>
        <label>
            Production Date:
            <InputDate @bind-Value="starship.ProductionDate" disabled="@disabled" />
        </label>
    </p>

    <button type="submit" disabled="@disabled">Submit</button>

    <p style="@messageStyles">
        @message
    </p>

    <p>
        <a href="http://www.startrek.com/">Star Trek</a>,
        &copy;1966-2019 CBS Studios, Inc. and
        <a href="https://www.paramount.com">Paramount Pictures</a>
    </p>
</EditForm>

@code {
    private bool disabled;
    private string message;
    private string messageStyles = "visibility:hidden";
    private CustomValidator customValidator;
    private Starship starship = new Starship() { ProductionDate = DateTime.UtcNow };

    private async Task HandleValidSubmit(EditContext editContext)
    {
        customValidator.ClearErrors();

        try
        {
            var response = await Http.PostAsJsonAsync<Starship>(
                "StarshipValidation", (Starship)editContext.Model);

            var errors = await response.Content
                .ReadFromJsonAsync<Dictionary<string, List<string>>>();

            if (response.StatusCode == HttpStatusCode.BadRequest && 
                errors.Count() > 0)
            {
                customValidator.DisplayErrors(errors);
            }
            else if (!response.IsSuccessStatusCode)
            {
                throw new HttpRequestException(
                    $"Validation failed. Status Code: {response.StatusCode}");
            }
            else
            {
                disabled = true;
                messageStyles = "color:green";
                message = "The form has been processed.";
            }
        }
        catch (AccessTokenNotAvailableException ex)
        {
            ex.Redirect();
        }
        catch (Exception ex)
        {
            Logger.LogError("Form processing error: {MESSAGE}", ex.Message);
            disabled = true;
            messageStyles = "color:red";
            message = "There was an error processing the form.";
        }
    }
}

Note

As an alternative to validation components, data annotation validation attributes can be used. Custom attributes applied to the form's model activate with the use of the DataAnnotationsValidator component. When used with server-side validation, the attributes must be executable on the server. For more information, see Model validation in ASP.NET Core MVC.

Note

The server-side validation approach in this section is suitable for any of the Blazor WebAssembly hosted solution examples in this documentation set:

InputText based on the input event

Use the InputText component to create a custom component that uses the input event instead of the change event.

In the following example, the CustomInputText component inherits the framework's InputText component and sets the event binding (CreateBinder) to the oninput event.

Shared/CustomInputText.razor:

@inherits InputText

<input 
    @attributes="AdditionalAttributes" 
    class="@CssClass" 
    value="@CurrentValue"
    @oninput="EventCallback.Factory.CreateBinder<string>(
         this, __value => CurrentValueAsString = __value, 
         CurrentValueAsString)" />

The CustomInputText component can be used anywhere InputText is used:

Pages/TestForm.razor:

@page "/testform"
@using System.ComponentModel.DataAnnotations;

<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <CustomInputText @bind-Value="exampleModel.Name" />

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

<p>
    CurrentValue: @exampleModel.Name
</p>

@code {
    private ExampleModel exampleModel = new ExampleModel();

    private void HandleValidSubmit()
    {
        ...
    }

    public class ExampleModel
    {
        [Required]
        [StringLength(10, ErrorMessage = "Name is too long.")]
        public string Name { get; set; }
    }
}

Radio buttons

Use InputRadio components with the InputRadioGroup component to create a radio button group. In the following example, properties are added to the Starship model described in the Built-in forms components section:

[Required]
[Range(typeof(Manufacturer), nameof(Manufacturer.SpaceX), 
    nameof(Manufacturer.VirginGalactic), ErrorMessage = "Pick a manufacturer.")]
public Manufacturer Manufacturer { get; set; } = Manufacturer.Unknown;

[Required, EnumDataType(typeof(Color))]
public Color? Color { get; set; } = null;

[Required, EnumDataType(typeof(Engine))]
public Engine? Engine { get; set; } = null;

Add the following enums to the app. Create a new file to hold the enums or add the enums to the Starship.cs file. Make the enums accessible to the Starship model and the Starfleet Starship Database form:

public enum Manufacturer { SpaceX, NASA, ULA, Virgin, Unknown }
public enum Color { ImperialRed, SpacecruiserGreen, StarshipBlue, VoyagerOrange }
public enum Engine { Ion, Plasma, Fusion, Warp }

Update the Starfleet Starship Database form described in the Built-in forms components section. Add the components to produce:

  • A radio button group for the ship manufacturer.
  • A nested radio button group for ship color and engine.
<p>
    <InputRadioGroup @bind-Value="starship.Manufacturer">
        Manufacturer:
        <br>
        @foreach (var manufacturer in (Manufacturer[])Enum
            .GetValues(typeof(Manufacturer)))
        {
            <InputRadio Value="manufacturer" />
            @manufacturer
            <br>
        }
    </InputRadioGroup>
</p>

<p>
    Pick one color and one engine:
    <InputRadioGroup Name="engine" @bind-Value="starship.Engine">
        <InputRadioGroup Name="color" @bind-Value="starship.Color">
            <InputRadio Name="color" Value="Color.ImperialRed" />Imperial Red<br>
            <InputRadio Name="engine" Value="Engine.Ion" />Ion<br>
            <InputRadio Name="color" Value="Color.SpacecruiserGreen" />
                Spacecruiser Green<br>
            <InputRadio Name="engine" Value="Engine.Plasma" />Plasma<br>
            <InputRadio Name="color" Value="Color.StarshipBlue" />Starship Blue<br>
            <InputRadio Name="engine" Value="Engine.Fusion" />Fusion<br>
            <InputRadio Name="color" Value="Color.VoyagerOrange" />
                Voyager Orange<br>
            <InputRadio Name="engine" Value="Engine.Warp" />Warp<br>
        </InputRadioGroup>
    </InputRadioGroup>
</p>

Note

If Name is omitted, InputRadio components are grouped by their most recent ancestor.

When working with radio buttons in a form, data binding is handled differently than other elements because radio buttons are evaluated as a group. The value of each radio button is fixed, but the value of the radio button group is the value of the selected radio button. The following example shows how to:

  • Handle data binding for a radio button group.
  • Support validation using a custom InputRadio component.
@using System.Globalization
@typeparam TValue
@inherits InputBase<TValue>

<input @attributes="AdditionalAttributes" type="radio" value="@SelectedValue" 
       checked="@(SelectedValue.Equals(Value))" @onchange="OnChange" />

@code {
    [Parameter]
    public TValue SelectedValue { get; set; }

    private void OnChange(ChangeEventArgs args)
    {
        CurrentValueAsString = args.Value.ToString();
    }

    protected override bool TryParseValueFromString(string value, 
        out TValue result, out string errorMessage)
    {
        var success = BindConverter.TryConvertTo<TValue>(
            value, CultureInfo.CurrentCulture, out var parsedValue);
        if (success)
        {
            result = parsedValue;
            errorMessage = null;

            return true;
        }
        else
        {
            result = default;
            errorMessage = $"{FieldIdentifier.FieldName} field isn't valid.";

            return false;
        }
    }
}

The following EditForm uses the preceding InputRadio component to obtain and validate a rating from the user:

@page "/RadioButtonExample"
@using System.ComponentModel.DataAnnotations

<h1>Radio Button Group Test</h1>

<EditForm Model="@model" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    @for (int i = 1; i <= 5; i++)
    {
        <label>
            <InputRadio name="rate" SelectedValue="i" @bind-Value="model.Rating" />
            @i
        </label>
    }

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

<p>You chose: @model.Rating</p>

@code {
    private Model model = new Model();

    private void HandleValidSubmit()
    {
        ...
    }

    public class Model
    {
        [Range(1, 5)]
        public int Rating { get; set; }
    }
}

Binding <select> element options to C# object null values

There's no sensible way to represent a <select> element option value as a C# object null value, because:

  • HTML attributes can't have null values. The closest equivalent to null in HTML is absence of the HTML value attribute from the <option> element.
  • When selecting an <option> with no value attribute, the browser treats the value as the text content of that <option>'s element.

The Blazor framework doesn't attempt to suppress the default behavior because it would involve:

  • Creating a chain of special-case workarounds in the framework.
  • Breaking changes to current framework behavior.

The most plausible null equivalent in HTML is an empty string value. The Blazor framework handles null to empty string conversions for two-way binding to a <select>'s value.

The Blazor framework doesn't automatically handle null to empty string conversions when attempting two-way binding to a <select>'s value. For more information, see Fix binding <select> to a null value (dotnet/aspnetcore #23221).

Validation support

The DataAnnotationsValidator component attaches validation support using data annotations to the cascaded EditContext. Enabling support for validation using data annotations requires this explicit gesture. To use a different validation system than data annotations, replace the DataAnnotationsValidator with a custom implementation. The ASP.NET Core implementation is available for inspection in the reference source: DataAnnotationsValidator/AddDataAnnotationsValidation. The preceding links to reference source provide code from the repository's master branch, which represents the product unit's current development for the next release of ASP.NET Core. To select the branch for a different release, use the GitHub branch selector (for example release/3.1).

Blazor performs two types of validation:

  • Field validation is performed when the user tabs out of a field. During field validation, the DataAnnotationsValidator component associates all reported validation results with the field.
  • Model validation is performed when the user submits the form. During model validation, the DataAnnotationsValidator component attempts to determine the field based on the member name that the validation result reports. Validation results that aren't associated with an individual member are associated with the model rather than a field.

Validation Summary and Validation Message components

The ValidationSummary component summarizes all validation messages, which is similar to the Validation Summary Tag Helper:

<ValidationSummary />

Output validation messages for a specific model with the Model parameter:

<ValidationSummary Model="@starship" />

The ValidationMessage<TValue> component displays validation messages for a specific field, which is similar to the Validation Message Tag Helper. Specify the field for validation with the For attribute and a lambda expression naming the model property:

<ValidationMessage For="@(() => starship.MaximumAccommodation)" />

The ValidationMessage<TValue> and ValidationSummary components support arbitrary attributes. Any attribute that doesn't match a component parameter is added to the generated <div> or <ul> element.

Control the style of validation messages in the app's stylesheet (wwwroot/css/app.css or wwwroot/css/site.css). The default validation-message class sets the text color of validation messages to red:

.validation-message {
    color: red;
}

Custom validation attributes

To ensure that a validation result is correctly associated with a field when using a custom validation attribute, pass the validation context's MemberName when creating the ValidationResult:

using System;
using System.ComponentModel.DataAnnotations;

private class CustomValidator : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, 
        ValidationContext validationContext)
    {
        ...

        return new ValidationResult("Validation message to user.",
            new[] { validationContext.MemberName });
    }
}

Note

ValidationContext.GetService is null. Injecting services for validation in the IsValid method isn't supported.

Custom validation class attributes

Custom validation class names are useful when integrating with CSS frameworks, such as Bootstrap. To specify custom validation class names, create a class derived from FieldCssClassProvider and set the class on the EditContext instance:

var editContext = new EditContext(model);
editContext.SetFieldCssClassProvider(new MyFieldClassProvider());

...

private class MyFieldClassProvider : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext, 
        in FieldIdentifier fieldIdentifier)
    {
        var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

        return isValid ? "good field" : "bad field";
    }
}

Blazor data annotations validation package

The Microsoft.AspNetCore.Components.DataAnnotations.Validation is a package that fills validation experience gaps using the DataAnnotationsValidator component. The package is currently experimental.

Note

The Microsoft.AspNetCore.Components.DataAnnotations.Validation package has a latest version of release candidate at Nuget.org. Continue to use the experimental release candidate package at this time. The package's assembly might be moved to either the framework or the runtime in a future release. Watch the Announcements GitHub repository, the dotnet/aspnetcore GitHub repository, or this topic section for further updates.

[CompareProperty] attribute

The CompareAttribute doesn't work well with the DataAnnotationsValidator component because it doesn't associate the validation result with a specific member. This can result in inconsistent behavior between field-level validation and when the entire model is validated on a submit. The Microsoft.AspNetCore.Components.DataAnnotations.Validation experimental package introduces an additional validation attribute, ComparePropertyAttribute, that works around these limitations. In a Blazor app, [CompareProperty] is a direct replacement for the [Compare] attribute.

Nested models, collection types, and complex types

Blazor provides support for validating form input using data annotations with the built-in DataAnnotationsValidator. However, the DataAnnotationsValidator only validates top-level properties of the model bound to the form that aren't collection- or complex-type properties.

To validate the bound model's entire object graph, including collection- and complex-type properties, use the ObjectGraphDataAnnotationsValidator provided by the experimental Microsoft.AspNetCore.Components.DataAnnotations.Validation package:

<EditForm Model="@model" OnValidSubmit="@HandleValidSubmit">
    <ObjectGraphDataAnnotationsValidator />
    ...
</EditForm>

Annotate model properties with [ValidateComplexType]. In the following model classes, the ShipDescription class contains additional data annotations to validate when the model is bound to the form:

Starship.cs:

using System;
using System.ComponentModel.DataAnnotations;

public class Starship
{
    ...

    [ValidateComplexType]
    public ShipDescription ShipDescription { get; set; } = 
        new ShipDescription();

    ...
}

ShipDescription.cs:

using System;
using System.ComponentModel.DataAnnotations;

public class ShipDescription
{
    [Required]
    [StringLength(40, ErrorMessage = "Description too long (40 char).")]
    public string ShortDescription { get; set; }

    [Required]
    [StringLength(240, ErrorMessage = "Description too long (240 char).")]
    public string LongDescription { get; set; }
}

Enable the submit button based on form validation

To enable and disable the submit button based on form validation:

  • Use the form's EditContext to assign the model when the component is initialized.
  • Validate the form in the context's OnFieldChanged callback to enable and disable the submit button.
  • Unhook the event handler in the Dispose method. For more information, see ASP.NET Core Blazor lifecycle.

Note

When using an EditContext, don't also assign a Model to the EditForm.

@implements IDisposable

<EditForm EditContext="@editContext">
    <DataAnnotationsValidator />
    <ValidationSummary />

    ...

    <button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>

@code {
    private Starship starship = new Starship() { ProductionDate = DateTime.UtcNow };
    private bool formInvalid = true;
    private EditContext editContext;

    protected override void OnInitialized()
    {
        editContext = new EditContext(starship);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
    {
        formInvalid = !editContext.Validate();
        StateHasChanged();
    }

    public void Dispose()
    {
        editContext.OnFieldChanged -= HandleFieldChanged;
    }
}

In the preceding example, set formInvalid to false if:

  • The form is preloaded with valid default values.
  • You want the submit button enabled when the form loads.

A side effect of the preceding approach is that a ValidationSummary component is populated with invalid fields after the user interacts with any one field. This scenario can be addressed in either of the following ways:

  • Don't use a ValidationSummary component on the form.
  • Make the ValidationSummary component visible when the submit button is selected (for example, in a HandleValidSubmit method).
<EditForm EditContext="@editContext" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary style="@displaySummary" />

    ...

    <button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>

@code {
    private string displaySummary = "display:none";

    ...

    private void HandleValidSubmit()
    {
        displaySummary = "display:block";
    }
}

Troubleshoot

InvalidOperationException: EditForm requires a Model parameter, or an EditContext parameter, but not both.

Confirm that the EditForm has a Model or EditContext. Don't use both for the same form.

When assigning a Model to the form, confirm that the model type is instantiated, as the following example shows:

private ExampleModel exampleModel = new ExampleModel();

Additional resources