ASP.NET Core Blazor forms and validation

By Daniel Roth 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()
    {
        Console.WriteLine("OnValidSubmit");
    }
}

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 model property (_exampleModel.Name) to the InputText component's Value property.
    • A change event delegate to the InputText component's ValueChanged property.
  • The DataAnnotationsValidator component attaches validation support using data annotations.
  • The ValidationSummary component summarizes validation messages.
  • HandleValidSubmit is triggered when the form successfully submits (passes validation).

A set of built-in input 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…
InputText <input>
InputTextArea <textarea>
InputSelect <select>
InputNumber <input type="number">
InputCheckbox <input type="checkbox">
InputDate <input type="date">

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 on edit and changing their CSS class to reflect the field state. Some components include useful parsing logic. For example, InputDate and InputNumber handle unparseable values gracefully by registering them 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();

    private void HandleValidSubmit()
    {
        Console.WriteLine("OnValidSubmit");
    }
}

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. The EditForm also provides convenient events for valid and invalid submits (OnValidSubmit, OnInvalidSubmit). Alternatively, use OnSubmit to trigger the validation and check field values with custom validation code.

In the following example:

  • The HandleSubmit method runs when the Submit button is selected.
  • The form is validated using the form's EditContext.
  • The form is further validated by passing the EditContext to the ServerValidate method that calls a web API endpoint on the server (not shown).
  • Additional code is run depending on the result of the client- and server-side validation by checking isValid.
<EditForm EditContext="@_editContext" OnSubmit="@HandleSubmit">

    ...

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

@code {
    private Starship _starship = new Starship();
    private EditContext _editContext;

    protected override void OnInitialized()
    {
        _editContext = new EditContext(_starship);
    }

    private async Task HandleSubmit()
    {
        var isValid = _editContext.Validate() && 
            await ServerValidate(_editContext);

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

    private async Task<bool> ServerValidate(EditContext editContext)
    {
        var serverChecksValid = ...

        return serverChecksValid;
    }
}

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.

Create a component with the following markup, and use the component just as InputText is used:

@inherits InputText

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

Work with radio buttons

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()
    {
        Console.WriteLine("valid");
    }

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

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.

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 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 and ValidationSummary components support arbitrary attributes. Any attribute that doesn't match a component parameter is added to the generated <div> or <ul> element.

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 MyCustomValidator : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, 
        ValidationContext validationContext)
    {
        ...

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

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.

[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; }

    ...
}

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.
@implements IDisposable

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

    ...

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

@code {
    private Starship _starship = new Starship();
    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";
    }
}