ASP.NET Core Blazor forms and input components

Note

This isn't the latest version of this article. To switch to the latest, use the ASP.NET Core version selector at the top of the table of contents.

Version selector

If the selector isn't visible in a narrow browser window, widen the window or select the vertical ellipsis () > Table of contents.

Table of contents selector

The Blazor framework supports forms and provides built-in input components:

The Microsoft.AspNetCore.Components.Forms namespace provides:

  • Classes for managing form elements, state, and validation.
  • Access to built-in Input* components, which can be used in Blazor apps.

A project created from the Blazor project template includes the namespace by default in the app's _Imports.razor file, which makes the namespace available in all of the Razor component files (.razor) of the app without explicit @using directives:

@using Microsoft.AspNetCore.Components.Forms

To demonstrate how an EditForm component works, consider the following example. ExampleModel represents the data model bound to the form and defines a Name property, which is used to store the value of the form's name field provided by the user.

ExampleModel.cs:

public class ExampleModel
{
    public string? Name { get; set; }
}

To demonstrate how an EditForm component works with data annotations validation, consider the following ExampleModel type. The Name property is marked required with the RequiredAttribute and specifies a StringLengthAttribute maximum string length limit and error message.

ExampleModel.cs:

using System.ComponentModel.DataAnnotations;

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

To demonstrate how an EditForm component works with data annotations validation, consider the following ExampleModel type. The Name property is marked required with the RequiredAttribute and specifies a StringLengthAttribute maximum string length limit and error message.

ExampleModel.cs:

using System.ComponentModel.DataAnnotations;

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

To demonstrate how an EditForm component works with data annotations validation, consider the following ExampleModel type. The Name property is marked required with the RequiredAttribute and specifies a StringLengthAttribute maximum string length limit and error message.

ExampleModel.cs:

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 Blazor framework's EditForm component. The following Razor component demonstrates typical elements, components, and Razor code to render a webform using an EditForm component, which is bound to the preceding ExampleModel type.

Pages/FormExample1.razor:

@page "/form-example-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample1> Logger

<EditForm Model="@exampleModel" OnSubmit="@HandleSubmit">
    <InputText id="name" @bind-Value="exampleModel.Name" />

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

@code {
    private ExampleModel exampleModel = new();

    private void HandleSubmit()
    {
        Logger.LogInformation("HandleSubmit called");

        // Process the form
    }
}

In the preceding FormExample1 component:

  • The EditForm component is rendered where the <EditForm> element appears.
  • The model is created in the component's @code block and held in a private field (exampleModel). The field is assigned to EditForm.Model's attribute (Model) of the <EditForm> element.
  • The InputText component (id="name") is an input component for editing string values. The @bind-Value directive attribute binds the exampleModel.Name model property to the InputText component's Value property.
  • The HandleSubmit method is registered as a handler for the OnSubmit callback. The handler is called when the form is submitted by the user.

To demonstrate how the preceding EditForm component works with data annotations validation:

ExampleModel.cs:

using System.ComponentModel.DataAnnotations;

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

The earlier FormExample1 component is modified:

  • OnSubmit is replaced with OnValidSubmit, which processes assigned event handler if the form is valid when submitted by the user. The method name is changed to HandleValidSubmit, which reflects that the method is called when the form is valid.
  • A ValidationSummary component is added to display validation messages when the form is invalid on form submission.
  • The data annotations validator (DataAnnotationsValidator component†) attaches validation support using data annotations:
    • If the <input> form field is left blank when the Submit button is selected, an error appears in the validation summary (ValidationSummary component‡) ("The Name field is required.") and HandleValidSubmit is not called.
    • If the <input> form field contains more than ten characters when the Submit button is selected, an error appears in the validation summary ("Name is too long.") and HandleValidSubmit is not called.
    • If the <input> form field contains a valid value when the Submit button is selected, HandleValidSubmit is called.

†The DataAnnotationsValidator component is covered in the Validator component section. ‡The ValidationSummary component is covered in the Validation Summary and Validation Message components section. For more information on property binding, see ASP.NET Core Blazor data binding.

Pages/FormExample1.razor:

@page "/form-example-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample1> Logger

<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();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}
@page "/form-example-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample1> Logger

<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();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}
@page "/form-example-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample1> Logger

<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();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}
@page "/form-example-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample1> Logger

<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()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}

In the preceding FormExample1 component:

  • The EditForm component is rendered where the <EditForm> element appears.
  • The model is created in the component's @code block and held in a private field (exampleModel). The field is assigned to EditForm.Model's attribute (Model) of the <EditForm> element.
  • The InputText component (id="name") is an input component for editing string values. The @bind-Value directive attribute binds the exampleModel.Name model property to the InputText component's Value property.
  • The HandleValidSubmit method is assigned to OnValidSubmit. The handler is called if the form passes validation.
  • The data annotations validator (DataAnnotationsValidator component†) attaches validation support using data annotations:
    • If the <input> form field is left blank when the Submit button is selected, an error appears in the validation summary (ValidationSummary component‡) ("The Name field is required.") and HandleValidSubmit is not called.
    • If the <input> form field contains more than ten characters when the Submit button is selected, an error appears in the validation summary ("Name is too long.") and HandleValidSubmit is not called.
    • If the <input> form field contains a valid value when the Submit button is selected, HandleValidSubmit is called.

†The DataAnnotationsValidator component is covered in the Validator component section. ‡The ValidationSummary component is covered in the Validation Summary and Validation Message components section. For more information on property binding, see ASP.NET Core Blazor data binding.

Binding a form

An EditForm creates an EditContext based on the assigned model instance as a cascading value for other components in the form. The EditContext tracks metadata about the edit process, including which fields have been modified and the current validation messages. Assigning to either an EditForm.Model or an EditForm.EditContext can bind a form to data.

Assignment to EditForm.Model:

<EditForm Model="@exampleModel" ...>

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

Assignment to EditForm.EditContext:

<EditForm EditContext="@editContext" ...>

@code {
    private ExampleModel exampleModel = new() { ... };
    private EditContext? editContext;

    protected override void OnInitialized()
    {
        editContext = new(exampleModel);
    }
}
<EditForm EditContext="@editContext" ...>

@code {
    private ExampleModel exampleModel = new() { ... };
    private EditContext editContext;

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

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

Unhandled exception rendering component: EditForm requires a Model parameter, or an EditContext parameter, but not both.

Handle form submission

The EditForm provides the following callbacks for handling form submission:

  • Use OnValidSubmit to assign an event handler to run when a form with valid fields is submitted.
  • Use OnInvalidSubmit to assign an event handler to run when a form with invalid fields is submitted.
  • Use OnSubmit to assign an event handler to run regardless of the form fields' validation status. The form is validated by calling EditContext.Validate in the event handler method. If Validate returns true, the form is valid.

Built-in input components

The Blazor framework provides built-in input components to receive and validate user input. The built-in input components in the following table are supported in an EditForm with an EditContext.

The components in the table are also supported outside of a form in Razor component markup. Inputs are validated when they're changed and when a form is submitted.

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

For more information on the InputFile component, see ASP.NET Core Blazor file uploads.

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

InputRadio<TValue> and InputRadioGroup<TValue> 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:

  • For input components in a form with an EditContext, the default validation behavior includes updating the field CSS class to reflect the field's state as valid or invalid with validation styling of the underlying HTML element.
  • For controls that don't have an EditContext, the default validation reflects the valid or invalid state but does not provide validation styling to the underlying HTML element.

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? for a nullable integer).

For more information on the InputFile component, see ASP.NET Core Blazor file uploads.

Example form

The following Starship type, which is used in several of this article's examples, defines a diverse set of properties with data annotations:

  • Identifier is required because it's annotated with the RequiredAttribute. Identifier requires a value of at least one character but no more than 16 characters using the StringLengthAttribute.
  • Description is optional because it isn't annotated with the RequiredAttribute.
  • Classification is required.
  • The MaximumAccommodation property defaults to zero but requires a value from one to 100,000 per its RangeAttribute.
  • IsValidatedDesign requires that the property have a true value, which matches a selected state when the property is bound to a checkbox in the UI (<input type="checkbox">).
  • ProductionDate is a DateTime and required.

Starship.cs:

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

The following form accepts and validates user input using:

Pages/FormExample2.razor:

@page "/form-example-2"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample2> Logger

<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>, 
        ©1966-2019 CBS Studios, Inc. and 
        <a href="https://www.paramount.com">Paramount Pictures</a>
    </p>
</EditForm>

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

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}
@page "/form-example-2"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample2> Logger

<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>, 
        ©1966-2019 CBS Studios, Inc. and 
        <a href="https://www.paramount.com">Paramount Pictures</a>
    </p>
</EditForm>

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

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}
@page "/form-example-2"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample2> Logger

<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>, 
        ©1966-2019 CBS Studios, Inc. and 
        <a href="https://www.paramount.com">Paramount Pictures</a>
    </p>
</EditForm>

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

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}
@page "/form-example-2"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample2> Logger

<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>, 
        ©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()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}

The EditForm in the preceding example creates an EditContext based on the assigned Starship instance (Model="@starship") and handles a valid form. The next example (FormExample3 component) demonstrates how to assign an EditContext to a form and validate when the form is submitted.

In the following example:

  • A shortened version of the preceding Starfleet Starship Database form (FormExample2 component) is used that only accepts a value for the starship's identifier. The other Starship properties receive valid default values when an instance of the Starship type is created.
  • The HandleSubmit method executes when the Submit button is selected.
  • The form is validated by calling EditContext.Validate in the HandleSubmit method.
  • Logging is executed depending on the validation result.

Note

HandleSubmit in the FormExample3 component is demonstrated as an asynchronous method because storing form values often uses asynchronous calls (await ...). If the form is used in a test app as shown, HandleSubmit merely runs synchronously. For testing purposes, ignore the following build warning:

This async method lacks 'await' operators and will run synchronously. ...

Pages/FormExample3.razor:

@page "/form-example-3"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample3> Logger

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

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>

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

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

@code {
    private Starship starship = 
        new()
        {
            Identifier = "NCC-1701",
            Classification = "Exploration",
            MaximumAccommodation = 150,
            IsValidatedDesign = true,
            ProductionDate = new DateTime(2245, 4, 11)
        };
    private EditContext? editContext;

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

    private async Task HandleSubmit()
    {
        if (editContext != null && editContext.Validate())
        {
            Logger.LogInformation("HandleSubmit called: Form is valid");

            // Process the valid form
            // await ...
            await Task.CompletedTask;
        }
        else
        {
            Logger.LogInformation("HandleSubmit called: Form is INVALID");
        }
    }
}
@page "/form-example-3"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample3> Logger

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

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>

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

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

@code {
    private Starship starship = 
        new()
        {
            Identifier = "NCC-1701",
            Classification = "Exploration",
            MaximumAccommodation = 150,
            IsValidatedDesign = true,
            ProductionDate = new DateTime(2245, 4, 11)
        };
    private EditContext? editContext;

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

    private async Task HandleSubmit()
    {
        if (editContext != null && editContext.Validate())
        {
            Logger.LogInformation("HandleSubmit called: Form is valid");

            // Process the valid form
            // await ...
            await Task.CompletedTask;
        }
        else
        {
            Logger.LogInformation("HandleSubmit called: Form is INVALID");
        }
    }
}
@page "/form-example-3"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample3> Logger

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

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>

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

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

@code {
    private Starship starship = 
        new()
        {
            Identifier = "NCC-1701",
            Classification = "Exploration",
            MaximumAccommodation = 150,
            IsValidatedDesign = true,
            ProductionDate = new DateTime(2245, 4, 11)
        };
    private EditContext editContext;

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

    private async Task HandleSubmit()
    {
        if (editContext.Validate())
        {
            Logger.LogInformation("HandleSubmit called: Form is valid");

            // Process the valid form
            // await ...
        }
        else
        {
            Logger.LogInformation("HandleSubmit called: Form is INVALID");
        }
    }
}
@page "/form-example-3"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample3> Logger

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

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>

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

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

@code {
    private Starship starship = 
        new Starship()
        {
            Identifier = "NCC-1701",
            Classification = "Exploration",
            MaximumAccommodation = 150,
            IsValidatedDesign = true,
            ProductionDate = new DateTime(2245, 4, 11)
        };
    private EditContext editContext;

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

    private async Task HandleSubmit()
    {
        if (editContext.Validate())
        {
            Logger.LogInformation("HandleSubmit called: Form is valid");

            // Process the valid form
            // await ...
        }
        else
        {
            Logger.LogInformation("HandleSubmit called: Form is INVALID");
        }
    }
}

Note

Changing the EditContext after it's assigned is not supported.

Multiple option selection with the InputSelect component

Binding supports multiple option selection with the InputSelect<TValue> component. The @onchange event provides an array of the selected options via event arguments (ChangeEventArgs). The value must be bound to an array type, and binding to an array type makes the multiple attribute optional on the InputSelect<TValue> tag.

In the following example, the user must select at least two starship classifications but no more than three classifications.

Pages/BindMultipleWithInputSelect.razor:

@page "/bind-multiple-with-inputselect"
@using System.ComponentModel.DataAnnotations 
@using Microsoft.Extensions.Logging
@inject ILogger<BindMultipleWithInputSelect> Logger 

<h1>Bind Multiple <code>InputSelect</code>Example</h1>

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

    <p>
        <label>
            Select classifications (Minimum: 2, Maximum: 3):
            <InputSelect @bind-Value="starship.SelectedClassification">
                <option value="@Classification.Exploration">Exploration</option>
                <option value="@Classification.Diplomacy">Diplomacy</option>
                <option value="@Classification.Defense">Defense</option>
                <option value="@Classification.Research">Research</option>
            </InputSelect>
        </label>
    </p>

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

<p>
    Selected Classifications: 
    @string.Join(", ", starship.SelectedClassification)
</p>

@code {
    private EditContext? editContext;
    private Starship starship = new();

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

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");
    }

    private class Starship
    {
        [Required, MinLength(2), MaxLength(3)]
        public Classification[] SelectedClassification { get; set; } =
            new[] { Classification.Diplomacy };
    }

    private enum Classification { Exploration, Diplomacy, Defense, Research }
}

For information on how empty strings and null values are handled in data binding, see the Binding InputSelect options to C# object null values section.

Binding InputSelect options to C# object null values

For information on how empty strings and null values are handled in data binding, see ASP.NET Core Blazor data binding.

Display name support

Several built-in components support display names with the InputBase<TValue>.DisplayName parameter.

In the Starfleet Starship Database form (FormExample2 component) of the Example form section, the production date of a new starship doesn't specify a display name:

<label>
    Production Date:
    <InputDate @bind-Value="starship.ProductionDate" />
</label>

If the field contains an invalid date when the form is submitted, the error message doesn't display a friendly name. The field name, "ProductionDate" doesn't have a space between "Production" and "Date" when it appears in the validation summary:

The ProductionDate field must be a date.

Set the DisplayName property to a friendly name with a space between the words "Production" and "Date":

<label>
    Production Date:
    <InputDate @bind-Value="starship.ProductionDate" 
               DisplayName="Production Date" />
</label>

The validation summary displays the friendly name when the field's value is invalid:

The Production Date field must be a date.

Error message template support

InputDate<TValue> and InputNumber<TValue> support error message templates:

In the Starfleet Starship Database form (FormExample2 component) of the Example form section with a friendly display name assigned, the Production Date field produces an error message using the following default error message template:

The {0} field must be a date.

The position of the {0} placeholder is where the value of the DisplayName property appears when the error is displayed to the user.

<label>
    Production Date:
    <InputDate @bind-Value="starship.ProductionDate" 
               DisplayName="Production Date" />
</label>

The Production Date field must be a date.

Assign a custom template to ParsingErrorMessage to provide a custom message:

<label>
    Production Date:
    <InputDate @bind-Value="starship.ProductionDate" 
               DisplayName="Production Date" 
               ParsingErrorMessage="The {0} field has an incorrect date value." />
</label>

The Production Date field has an incorrect date value.

In the Starfleet Starship Database form (FormExample2 component) of the Example form section uses a default error message template:

The {0} field must be a date.

The position of the {0} placeholder is where the value of the DisplayName property appears when the error is displayed to the user.

<label>
    Production Date:
    <InputDate @bind-Value="starship.ProductionDate" />
</label>

The ProductionDate field must be a date.

Assign a custom template to ParsingErrorMessage to provide a custom message:

<label>
    Production Date:
    <InputDate @bind-Value="starship.ProductionDate" 
               ParsingErrorMessage="The {0} field has an incorrect date value." />
</label>

The ProductionDate field has an incorrect date value.

Basic validation

In basic form validation scenarios, an EditForm instance can use declared EditContext and ValidationMessageStore instances to validate form fields. A handler for the OnValidationRequested event of the EditContext executes custom validation logic. The handler's result updates the ValidationMessageStore instance.

Basic form validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a validator component is recommended where an independent model class is used across several components.

In the following FormExample4 component, the HandleValidationRequested handler method clears any existing validation messages by calling ValidationMessageStore.Clear before validating the form.

Pages/FormExample4.razor:

@page "/form-example-4"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample4> Logger

<h2>Ship Holodecks</h2>

<EditForm EditContext="editContext" OnValidSubmit="@HandleValidSubmit">
    <label>
        Type 1:
        <InputCheckbox @bind-Value="holodeck.Type1" />
    </label>

    <label>
        Type 2:
        <InputCheckbox @bind-Value="holodeck.Type2" />
    </label>

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

    <ValidationMessage For="() => holodeck.Options" />

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

@code {
    private EditContext? editContext;
    private Holodeck holodeck = new();
    private ValidationMessageStore? messageStore;

    protected override void OnInitialized()
    {
        editContext = new(holodeck);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

    private void HandleValidationRequested(object? sender, 
        ValidationRequestedEventArgs args)
    {
        messageStore?.Clear();

        // Custom validation logic
        if (!holodeck.Options)
        {
            messageStore?.Add(() => holodeck.Options, "Select at least one.");
        }
    }

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called: Processing the form");

        // Process the form
    }

    public class Holodeck
    {
        public bool Type1 { get; set; }
        public bool Type2 { get; set; }
        public bool Options => Type1 || Type2;
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnValidationRequested -= HandleValidationRequested;
        }
    }
}
@page "/form-example-4"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample4> Logger

<h2>Ship Holodecks</h2>

<EditForm EditContext="editContext" OnValidSubmit="@HandleValidSubmit">
    <label>
        Type 1:
        <InputCheckbox @bind-Value="holodeck.Type1" />
    </label>

    <label>
        Type 2:
        <InputCheckbox @bind-Value="holodeck.Type2" />
    </label>

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

    <ValidationMessage For="() => holodeck.Options" />

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

@code {
    private EditContext? editContext;
    private Holodeck holodeck = new();
    private ValidationMessageStore? messageStore;

    protected override void OnInitialized()
    {
        editContext = new(holodeck);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

    private void HandleValidationRequested(object? sender, 
        ValidationRequestedEventArgs args)
    {
        messageStore?.Clear();

        // Custom validation logic
        if (!holodeck.Options)
        {
            messageStore?.Add(() => holodeck.Options, "Select at least one.");
        }
    }

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called: Processing the form");

        // Process the form
    }

    public class Holodeck
    {
        public bool Type1 { get; set; }
        public bool Type2 { get; set; }
        public bool Options => Type1 || Type2;
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnValidationRequested -= HandleValidationRequested;
        }
    }
}
@page "/form-example-4"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample4> Logger

<h2>Ship Holodecks</h2>

<EditForm EditContext="editContext" OnValidSubmit="@HandleValidSubmit">
    <label>
        Type 1:
        <InputCheckbox @bind-Value="holodeck.Type1" />
    </label>

    <label>
        Type 2:
        <InputCheckbox @bind-Value="holodeck.Type2" />
    </label>

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

    <ValidationMessage For="() => holodeck.Options" />

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

@code {
    private EditContext editContext;
    private Holodeck holodeck = new();
    private ValidationMessageStore messageStore;

    protected override void OnInitialized()
    {
        editContext = new(holodeck);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new(editContext);
    }

    private void HandleValidationRequested(object sender, 
        ValidationRequestedEventArgs args)
    {
        messageStore.Clear();

        // Custom validation logic
        if (!holodeck.Options)
        {
            messageStore.Add(() => holodeck.Options, "Select at least one.");
        }
    }

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called: Processing the form");

        // Process the form
    }

    public class Holodeck
    {
        public bool Type1 { get; set; }
        public bool Type2 { get; set; }
        public bool Options => Type1 || Type2;
    }

    public void Dispose()
    {
        editContext.OnValidationRequested -= HandleValidationRequested;
    }
}
@page "/form-example-4"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample4> Logger

<h2>Ship Holodecks</h2>

<EditForm EditContext="editContext" OnValidSubmit="@HandleValidSubmit">
    <label>
        Type 1:
        <InputCheckbox @bind-Value="holodeck.Type1" />
    </label>

    <label>
        Type 2:
        <InputCheckbox @bind-Value="holodeck.Type2" />
    </label>

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

    <ValidationMessage For="() => holodeck.Options" />

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

@code {
    private EditContext editContext;
    private Holodeck holodeck = new Holodeck();
    private ValidationMessageStore messageStore;

    protected override void OnInitialized()
    {
        editContext = new EditContext(holodeck);
        editContext.OnValidationRequested += HandleValidationRequested;
        messageStore = new ValidationMessageStore(editContext);
    }

    private void HandleValidationRequested(object sender, 
        ValidationRequestedEventArgs args)
    {
        messageStore.Clear();

        // Custom validation logic
        if (!holodeck.Options)
        {
            messageStore.Add(() => holodeck.Options, "Select at least one.");
        }
    }

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called: Processing the form");

        // Process the form
    }

    public class Holodeck
    {
        public bool Type1 { get; set; }
        public bool Type2 { get; set; }
        public bool Options => Type1 || Type2;
    }

    public void Dispose()
    {
        editContext.OnValidationRequested -= HandleValidationRequested;
    }
}

Data Annotations Validator component and custom validation

The DataAnnotationsValidator component attaches data annotations validation to a cascaded EditContext. Enabling data annotations validation requires the DataAnnotationsValidator component. To use a different validation system than data annotations, use a custom implementation instead of the DataAnnotationsValidator component. The framework implementations for DataAnnotationsValidator are available for inspection in the reference source:

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the Switch branches or tags dropdown list. For more information, see How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205).

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.

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). You can 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, CustomValidation, 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.

CustomValidation.cs (if used in a test app, change the namespace, BlazorSample, to match the app's namespace):

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample;

public class CustomValidation : ComponentBase
{
    private ValidationMessageStore? messageStore;

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

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

        messageStore = new(CurrentEditContext);

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

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

            CurrentEditContext.NotifyValidationStateChanged();
        }
    }

    public void ClearErrors()
    {
        messageStore?.Clear();
        CurrentEditContext?.NotifyValidationStateChanged();
    }
}
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample;

public class CustomValidation : ComponentBase
{
    private ValidationMessageStore? messageStore;

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

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

        messageStore = new(CurrentEditContext);

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

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

            CurrentEditContext.NotifyValidationStateChanged();
        }
    }

    public void ClearErrors()
    {
        messageStore?.Clear();
        CurrentEditContext?.NotifyValidationStateChanged();
    }
}
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample
{
    public class CustomValidation : ComponentBase
    {
        private ValidationMessageStore messageStore;
    
        [CascadingParameter]
        private EditContext CurrentEditContext { get; set; }
    
        protected override void OnInitialized()
        {
            if (CurrentEditContext == null)
            {
                throw new InvalidOperationException(
                    $"{nameof(CustomValidation)} requires a cascading " +
                    $"parameter of type {nameof(EditContext)}. " +
                    $"For example, you can use {nameof(CustomValidation)} " +
                    $"inside an {nameof(EditForm)}.");
            }
    
            messageStore = new(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();
        }
    }
}
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample
{
    public class CustomValidation : ComponentBase
    {
        private ValidationMessageStore messageStore;
    
        [CascadingParameter]
        private EditContext CurrentEditContext { get; set; }
    
        protected override void OnInitialized()
        {
            if (CurrentEditContext == null)
            {
                throw new InvalidOperationException(
                    $"{nameof(CustomValidation)} requires a cascading " +
                    $"parameter of type {nameof(EditContext)}. " +
                    $"For example, you can use {nameof(CustomValidation)} " +
                    $"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();
        }
    }
}

Important

Specifying a namespace is required when deriving from ComponentBase. Failing to specify a namespace results in a build error:

Tag helpers cannot target tag name '<global namespace>.{CLASS NAME}' because it contains a ' ' character.

The {CLASS NAME} placeholder is the name of the component class. The custom validator example in this section specifies the example namespace BlazorSample.

Note

Anonymous lambda expressions are registered event handlers for OnValidationRequested and OnFieldChanged in the preceding example. It isn't necessary to implement IDisposable and unsubscribe the event delegates in this scenario. For more information, see ASP.NET Core Razor component lifecycle.

Business logic validation with a validator component

For general business logic validation, use a validator component that receives form errors in a dictionary.

Basic validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a validator component is recommended where an independent model class is used across several components.

In the following example:

  • A shortened version of the Starfleet Starship Database form (FormExample2 component) from the Example form section is used that only accepts the starship's classification and description. Data annotation validation is not triggered on form submission because the DataAnnotationsValidator component isn't included in the form.
  • The CustomValidation 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's validation summary.

Pages/FormExample5.razor:

@page "/form-example-5"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample5> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />

    <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>
            Description (optional):
            <InputTextArea @bind-Value="starship.Description" />
        </label>
    </p>

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

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

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

    private void HandleValidSubmit()
    {
        customValidation?.ClearErrors();

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

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

        if (errors.Any())
        {
            customValidation?.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("HandleValidSubmit called: Processing the form");

            // Process the valid form
        }
    }
}
@page "/form-example-5"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample5> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />

    <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>
            Description (optional):
            <InputTextArea @bind-Value="starship.Description" />
        </label>
    </p>

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

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

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

    private void HandleValidSubmit()
    {
        customValidation?.ClearErrors();

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

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

        if (errors.Any())
        {
            customValidation?.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("HandleValidSubmit called: Processing the form");

            // Process the valid form
        }
    }
}
@page "/form-example-5"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample5> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />

    <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>
            Description (optional):
            <InputTextArea @bind-Value="starship.Description" />
        </label>
    </p>

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

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

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

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

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

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

        if (errors.Any())
        {
            customValidation.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("HandleValidSubmit called: Processing the form");

            // Process the valid form
        }
    }
}
@page "/form-example-5"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample5> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />

    <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>
            Description (optional):
            <InputTextArea @bind-Value="starship.Description" />
        </label>
    </p>

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

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

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

    private void HandleValidSubmit()
    {
        customValidation.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.Any())
        {
            customValidation.DisplayErrors(errors);
        }
        else
        {
            Logger.LogInformation("HandleValidSubmit called: Processing the form");

            // Process the valid 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 with a validator component

Server validation is supported in addition to client-side validation:

  • 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.

Basic validation is useful in cases where the form's model is defined within the component hosting the form, either as members directly on the component or in a subclass. Use of a validator component is recommended where an independent model class is used across several components.

The following example is based on:

Place the Starship model (Starship.cs) into the solution's Shared project so that both the client and server apps can use the model. Add or update the namespace to match the namespace of the shared app (for example, namespace BlazorSample.Shared). Since the model requires data annotations, add the System.ComponentModel.Annotations package to the Shared project.

Note

For guidance on adding packages to .NET apps, see the articles under Install and manage packages at Package consumption workflow (NuGet documentation). Confirm correct package versions at NuGet.org.

In the Server project, add a controller to process starship validation requests and return failed validation messages. Update the namespaces in the last using statement for the Shared project and the namespace for the controller class. In addition to data annotations validation (client-side and server-side), the controller validates that a value is provided for the ship's description (Description) if the user selects the Defense ship classification (Classification).

The validation for the Defense ship classification only occurs server-side in the controller because the upcoming form doesn't perform the same validation client-side when the form is submitted to the server. Server-side validation without client-side validation is common in apps that require private business logic validation of user input on the server. For example, private information from data stored for a user might be required to validate user input. Private data obviously can't be sent to the client for client-side validation.

Note

The StarshipValidation controller in this section uses Microsoft Identity 2.0. The Web API only accepts tokens for users that have the "API.Access" scope for this API. Additional customization is required if the API's scope name is different from API.Access. For a version of the controller that works with Microsoft Identity 1.0 and ASP.NET Core prior to version 5.0, see an earlier version of this article.

For more information on security, see:

Controllers/StarshipValidation.cs:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Web.Resource;
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;
    }

    static readonly string[] scopeRequiredByApi = new[] { "API.Access" };

    [HttpPost]
    public async Task<IActionResult> Post(Starship starship)
    {
        HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

        try
        {
            if (starship.Classification == "Defense" && 
                string.IsNullOrEmpty(starship.Description))
            {
                ModelState.AddModelError(nameof(starship.Description),
                    "For a 'Defense' ship " +
                    "classification, 'Description' is required.");
            }
            else
            {
                logger.LogInformation("Processing the form asynchronously");

                // Process the valid form
                // async ...

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

        return BadRequest(ModelState);
    }
}
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Web.Resource;
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;
        }

        static readonly string[] scopeRequiredByApi = new[] { "API.Access" };

        [HttpPost]
        public async Task<IActionResult> Post(Starship starship)
        {
            HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

            try
            {
                if (starship.Classification == "Defense" && 
                    string.IsNullOrEmpty(starship.Description))
                {
                    ModelState.AddModelError(nameof(starship.Description),
                        "For a 'Defense' ship " +
                        "classification, 'Description' is required.");
                }
                else
                {
                    logger.LogInformation("Processing the form asynchronously");

                    // Process the valid form
                    // async ...

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

            return BadRequest(ModelState);
        }
    }
}

If using the preceding controller in a hosted Blazor WebAssembly app, update the namespace (BlazorSample.Server.Controllers) to match the app's controllers namespace.

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)."]
  }
}

Note

To demonstrate the preceding JSON response, you must either disable the form's client-side validation to permit empty field form submission or use a tool to send a request directly to the server API, such as Firefox Browser Developer or Postman.

If the server API returns the preceding default JSON response, it's possible for the client to parse the response in developer code to obtain the children of the errors node for forms validation error processing. It's inconvenient to write developer code to parse the file. Parsing the JSON manually requires producing a Dictionary<string, List<string>> of errors after calling ReadFromJsonAsync. 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 Program.cs. 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.

Add the Microsoft.AspNetCore.Mvc namespace to the top of the Program.cs file in the Server app:

using Microsoft.AspNetCore.Mvc;

In Program.cs, locate the AddControllersWithViews extension method and add the following call to ConfigureApiBehaviorOptions:

builder.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 CustomValidation component shown in the Validator components section. Update the namespace to match the app (for example, namespace BlazorSample.Client).

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

In the following FormExample6 component, update the namespace of the Shared project (@using BlazorSample.Shared) to the shared project's namespace. Note that the form requires authorization, so the user must be signed into the app to navigate to the form.

Pages/FormExample6.razor:

@page "/form-example-6"
@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<FormExample6> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="customValidation" />
    <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 CustomValidation? customValidation;
    private Starship starship = new() { ProductionDate = DateTime.UtcNow };

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

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

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

            if (response.StatusCode == HttpStatusCode.BadRequest && 
                errors.Any())
            {
                customValidation?.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.";
        }
    }
}
@page "/form-example-6"
@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<FormExample6> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="@starship" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="customValidation" />
    <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 CustomValidation customValidation;
    private Starship starship = new() { ProductionDate = DateTime.UtcNow };

    private async Task HandleValidSubmit(EditContext editContext)
    {
        customValidation.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.Any())
            {
                customValidation.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 the use of a validation component, 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 hosted Blazor WebAssembly solution examples in this documentation set:

InputText based on the input event

Use the InputText component to create a custom component that uses the oninput event (input) instead of the onchange event (change). Use of the input event triggers field validation on each keystroke.

The following example uses the ExampleModel class.

ExampleModel.cs:

using System.ComponentModel.DataAnnotations;

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

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

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

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

The following CustomInputText component inherits the framework's InputText component and sets event binding to the oninput event (input).

Shared/CustomInputText.razor:

@inherits InputText

<input @attributes="AdditionalAttributes" 
       class="@CssClass" 
       @bind="CurrentValueAsString" 
       @bind:event="oninput" />
@inherits InputText

<input @attributes="AdditionalAttributes" 
       class="@CssClass" 
       @bind="CurrentValueAsString" 
       @bind:event="oninput" />
@inherits InputText

<input @attributes="AdditionalAttributes" 
       class="@CssClass" 
       @bind="CurrentValueAsString" 
       @bind:event="oninput" />
@inherits InputText

<input @attributes="AdditionalAttributes" 
       class="@CssClass" 
       @bind="CurrentValueAsString" 
       @bind:event="oninput" />

The CustomInputText component can be used anywhere InputText is used. The following FormExample7 component uses the shared CustomInputText component.

Pages/FormExample7.razor:

@page "/form-example-7"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample7> Logger

<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();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}
@page "/form-example-7"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample7> Logger

<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();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}
@page "/form-example-7"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample7> Logger

<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();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}
@page "/form-example-7"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample7> Logger

<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()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}

Radio buttons

The example in this section is based on the Starfleet Starship Database form of the Example form section of this article.

Add the following enum types to the app. Create a new file to hold them or add them to the Starship.cs file.

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

Make the enums accessible to the:

  • Starship model in Starship.cs (for example, using static ComponentEnums; if the enums class is named ComponentEnums).
  • Starfleet Starship Database form (for example, @using static ComponentEnums if the enums class is named ComponentEnums).

Use InputRadio<TValue> components with the InputRadioGroup<TValue> component to create a radio button group. In the following example, properties are added to the Starship model described in the Example form 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;
[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;

Update the Starfleet Starship Database form (FormExample2 component) from the Example form section. Add the components to produce:

  • A radio button group for the ship manufacturer.
  • A nested radio button group for engine and ship color.

Note

Nested radio button groups aren't often used in forms because they can result in a disorganized layout of form controls that may confuse users. However, there are cases when they make sense in UI design, such as in the following example that pairs recommendations for two user inputs, ship engine and ship color. One engine and one color are required by the form's validation. The form's layout uses nested InputRadioGroup<TValue>s to pair engine and color recommendations. However, the user can combine any engine with any color to submit the form.

<fieldset>
    <legend>Manufacturer</legend>
    <InputRadioGroup @bind-Value="starship.Manufacturer">
        @foreach (var manufacturer in (Manufacturer[])Enum
            .GetValues(typeof(Manufacturer)))
        {
            <label>
                <InputRadio Value="@manufacturer" />
                <text>&nbsp;</text>@manufacturer
            </label>
        }
    </InputRadioGroup>
<fieldset>

<p>
    Select one engine and one color. Recommendations are paired but any 
    combination of engine and color is allowed:<br>
    <InputRadioGroup Name="engine" @bind-Value="starship.Engine">
        <InputRadioGroup Name="color" @bind-Value="starship.Color">
            <InputRadio Name="engine" Value="@Engine.Ion" />
            Engine: Ion<br>
            <InputRadio Name="color" Value="@Color.ImperialRed" />
            Color: Imperial Red<br><br>
            <InputRadio Name="engine" Value="@Engine.Plasma" />
            Engine: Plasma<br>
            <InputRadio Name="color" Value="@Color.SpacecruiserGreen" />
            Color: Spacecruiser Green<br><br>
            <InputRadio Name="engine" Value="@Engine.Fusion" />
            Engine: Fusion<br>
            <InputRadio Name="color" Value="@Color.StarshipBlue" />
            Color: Starship Blue<br><br>
            <InputRadio Name="engine" Value="@Engine.Warp" />
            Engine: Warp<br>
            <InputRadio Name="color" Value="@Color.VoyagerOrange" />
            Color: Voyager Orange
        </InputRadioGroup>
    </InputRadioGroup>
</p>

Note

If Name is omitted, InputRadio<TValue> 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<TValue> component.

Shared/InputRadio.razor:

@using System.Globalization
@inherits InputBase<TValue>
@typeparam 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;
        }
    }
}

For more information on generic type parameters (@typeparam), see the following articles:

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

Pages/RadioButtonExample.razor:

@page "/radio-button-example"
@using System.ComponentModel.DataAnnotations
@using Microsoft.Extensions.Logging
@inject ILogger<RadioButtonExample> Logger

<h1>Radio Button Example</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();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }

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

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.

CustomValidator.cs:

using System;
using System.ComponentModel.DataAnnotations;

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

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

Inject services into custom validation attributes through the ValidationContext. The following example demonstrates a salad chef form that validates user input with dependency injection (DI).

The SaladChef class indicates the approved fruit ingredient list for a salad.

SaladChef.cs:

public class SaladChef
{
    public string[] ThingsYouCanPutInASalad = { "Strawberries", "Pineapple", 
        "Honeydew", "Watermelon", "Grapes" };
}

Register SaladChef in the app's DI container in Program.cs:

builder.Services.AddTransient<SaladChef>();

The IsValid method of the following SaladChefValidatorAttribute class obtains the SaladChef service from DI to check the user's input.

SaladChefValidatorAttribute.cs:

using System.ComponentModel.DataAnnotations;

public class SaladChefValidatorAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(object? value,
        ValidationContext validationContext)
    {
        var saladChef = validationContext.GetRequiredService<SaladChef>();

        if (saladChef.ThingsYouCanPutInASalad.Contains(value?.ToString()))
        {
            return ValidationResult.Success;
        }

        return new ValidationResult("You should not put that in a salad!");
    }
}
using System.ComponentModel.DataAnnotations;

public class SaladChefValidatorAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value,
        ValidationContext validationContext)
    {
        var saladChef = validationContext.GetRequiredService<SaladChef>();

        if (saladChef.ThingsYouCanPutInASalad.Contains(value?.ToString()))
        {
            return ValidationResult.Success;
        }

        return new ValidationResult("You should not put that in a salad!");
    }
}

The following ValidationWithDI component validates user input by applying the SaladChefValidatorAttribute ([SaladChefValidator]) to the salad ingredient string (SaladIngredient).

Pages/ValidationWithDI.razor:

@page "/validation-with-di"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms

<EditForm Model="@this" autocomplete="off">
    <DataAnnotationsValidator />

    <p>
        Name something you can put in a salad:
        <input @bind="SaladIngredient" />
    </p>

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

    <ul>
        @foreach (var message in context.GetValidationMessages())
        {
            <li class="validation-message">@message</li>
        }
    </ul>

</EditForm>

@code {
    [SaladChefValidator]
    public string? SaladIngredient { get; set; }
}
@page "/validation-with-di"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms

<EditForm Model="@this" autocomplete="off">
    <DataAnnotationsValidator />

    <p>
        Name something you can put in a salad:
        <input @bind="SaladIngredient" />
    </p>

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

    <ul>
        @foreach (var message in context.GetValidationMessages())
        {
            <li class="validation-message">@message</li>
        }
    </ul>

</EditForm>

@code {
    [SaladChefValidator]
    public string SaladIngredient { get; set; }
}

Custom validation CSS class attributes

Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as Bootstrap.

The following example uses the ExampleModel class.

ExampleModel.cs:

using System.ComponentModel.DataAnnotations;

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

To specify custom validation CSS class attributes, start by providing CSS styles for custom validation. In the following example, valid (validField) and invalid (invalidField) styles are specified.

wwwroot/css/app.css (Blazor WebAssembly) or wwwroot/css/site.css (Blazor Server):

.validField {
    border-color: lawngreen;
}

.invalidField {
    background-color: tomato;
}

Create a class derived from FieldCssClassProvider that checks for field validation messages and applies the appropriate valid or invalid style.

CustomFieldClassProvider.cs:

using Microsoft.AspNetCore.Components.Forms;

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

        return isValid ? "validField" : "invalidField";
    }
}

Set the CustomFieldClassProvider class as the Field CSS Class Provider on the form's EditContext instance with SetFieldCssClassProvider.

Pages/FormExample8.razor:

@page "/form-example-8"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample8> Logger

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

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

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

@code {
    private ExampleModel exampleModel = new();
    private EditContext? editContext;

    protected override void OnInitialized()
    {
        editContext = new(exampleModel);
        editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
    }

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}

The preceding example checks the validity of all form fields and applies a style to each field. If the form should only apply custom styles to a subset of the fields, make CustomFieldClassProvider apply styles conditionally. The following CustomFieldClassProvider2 example only applies a style to the Name field. For any fields with names not matching Name, string.Empty is returned, and no style is applied.

CustomFieldClassProvider2.cs:

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider2 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        if (fieldIdentifier.FieldName == "Name")
        {
            var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

            return isValid ? "validField" : "invalidField";
        }

        return string.Empty;
    }
}

Add an additional property to ExampleModel, for example:

[StringLength(10, ErrorMessage = "Description is too long.")]
public string? Description { get; set; } 

Add the Description to the ExampleForm7 component's form:

<InputText id="description" @bind-Value="exampleModel.Description" />

Update the EditContext instance in the component's OnInitialized method to use the new Field CSS Class Provider:

editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2());

Because a CSS validation class isn't applied to the Description field (id="description"), it isn't styled. However, field validation runs normally. If more than 10 characters are provided, the validation summary indicates the error:

Description is too long.

In the following example:

  • The custom CSS style is applied to the Name field.

  • Any other fields apply logic similar to Blazor's default logic and using Blazor's default field CSS validation styles, modified with valid or invalid. Note that for the default styles, you don't need to add them to the app's stylesheet if the app is based on a Blazor project template. For apps not based on a Blazor project template, the default styles can be added to the app's stylesheet:

    .valid.modified:not([type=checkbox]) {
        outline: 1px solid #26b050;
    }
    
    .invalid {
        outline: 1px solid red;
    }
    

CustomFieldClassProvider3.cs:

using Microsoft.AspNetCore.Components.Forms;

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

        if (fieldIdentifier.FieldName == "Name")
        {
            return isValid ? "validField" : "invalidField";
        }
        else
        {
            if (editContext.IsModified(fieldIdentifier))
            {
                return isValid ? "modified valid" : "modified invalid";
            }
            else
            {
                return isValid ? "valid" : "invalid";
            }
        }
    }
}

Update the EditContext instance in the component's OnInitialized method to use the preceding Field CSS Class Provider:

editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3());

Using CustomFieldClassProvider3:

  • The Name field uses the app's custom validation CSS styles.
  • The Description field uses logic similar to Blazor's logic and Blazor's default field CSS validation styles.

Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as Bootstrap.

The following example uses the ExampleModel class.

ExampleModel.cs:

using System.ComponentModel.DataAnnotations;

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

To specify custom validation CSS class attributes, start by providing CSS styles for custom validation. In the following example, valid (validField) and invalid (invalidField) styles are specified.

wwwroot/css/app.css (Blazor WebAssembly) or wwwroot/css/site.css (Blazor Server):

.validField {
    border-color: lawngreen;
}

.invalidField {
    background-color: tomato;
}

Create a class derived from FieldCssClassProvider that checks for field validation messages and applies the appropriate valid or invalid style.

CustomFieldClassProvider.cs:

using Microsoft.AspNetCore.Components.Forms;

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

        return isValid ? "validField" : "invalidField";
    }
}

Set the CustomFieldClassProvider class as the Field CSS Class Provider on the form's EditContext instance with SetFieldCssClassProvider.

Pages/FormExample8.razor:

@page "/form-example-8"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample8> Logger

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

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

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

@code {
    private ExampleModel exampleModel = new();
    private EditContext? editContext;

    protected override void OnInitialized()
    {
        editContext = new(exampleModel);
        editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
    }

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}

The preceding example checks the validity of all form fields and applies a style to each field. If the form should only apply custom styles to a subset of the fields, make CustomFieldClassProvider apply styles conditionally. The following CustomFieldClassProvider2 example only applies a style to the Name field. For any fields with names not matching Name, string.Empty is returned, and no style is applied.

CustomFieldClassProvider2.cs:

using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider2 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        if (fieldIdentifier.FieldName == "Name")
        {
            var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

            return isValid ? "validField" : "invalidField";
        }

        return string.Empty;
    }
}

Add an additional property to ExampleModel, for example:

[StringLength(10, ErrorMessage = "Description is too long.")]
public string? Description { get; set; } 

Add the Description to the ExampleForm7 component's form:

<InputText id="description" @bind-Value="exampleModel.Description" />

Update the EditContext instance in the component's OnInitialized method to use the new Field CSS Class Provider:

editContext?.SetFieldCssClassProvider(new CustomFieldClassProvider2());

Because a CSS validation class isn't applied to the Description field (id="description"), it isn't styled. However, field validation runs normally. If more than 10 characters are provided, the validation summary indicates the error:

Description is too long.

In the following example:

  • The custom CSS style is applied to the Name field.

  • Any other fields apply logic similar to Blazor's default logic and using Blazor's default field CSS validation styles, modified with valid or invalid. Note that for the default styles, you don't need to add them to the app's stylesheet if the app is based on a Blazor project template. For apps not based on a Blazor project template, the default styles can be added to the app's stylesheet:

    .valid.modified:not([type=checkbox]) {
        outline: 1px solid #26b050;
    }
    
    .invalid {
        outline: 1px solid red;
    }
    

CustomFieldClassProvider3.cs:

using Microsoft.AspNetCore.Components.Forms;

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

        if (fieldIdentifier.FieldName == "Name")
        {
            return isValid ? "validField" : "invalidField";
        }
        else
        {
            if (editContext.IsModified(fieldIdentifier))
            {
                return isValid ? "modified valid" : "modified invalid";
            }
            else
            {
                return isValid ? "valid" : "invalid";
            }
        }
    }
}

Update the EditContext instance in the component's OnInitialized method to use the preceding Field CSS Class Provider:

editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3());

Using CustomFieldClassProvider3:

  • The Name field uses the app's custom validation CSS styles.
  • The Description field uses logic similar to Blazor's logic and Blazor's default field CSS validation styles.

Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as Bootstrap.

The following example uses the ExampleModel class.

ExampleModel.cs:

using System.ComponentModel.DataAnnotations;

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

To specify custom validation CSS class attributes, start by providing CSS styles for custom validation. In the following example, valid (validField) and invalid (invalidField) styles are specified.

wwwroot/css/app.css (Blazor WebAssembly) or wwwroot/css/site.css (Blazor Server):

.validField {
    border-color: lawngreen;
}

.invalidField {
    background-color: tomato;
}

Create a class derived from FieldCssClassProvider that checks for field validation messages and applies the appropriate valid or invalid style.

CustomFieldClassProvider.cs:

using System.Linq;
using Microsoft.AspNetCore.Components.Forms;

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

        return isValid ? "validField" : "invalidField";
    }
}

Set the CustomFieldClassProvider class as the Field CSS Class Provider on the form's EditContext instance with SetFieldCssClassProvider.

Pages/FormExample8.razor:

@page "/form-example-8"
@using Microsoft.Extensions.Logging
@inject ILogger<FormExample8> Logger

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

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

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

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

    protected override void OnInitialized()
    {
        editContext = new(exampleModel);
        editContext.SetFieldCssClassProvider(new CustomFieldClassProvider());
    }

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }
}

The preceding example checks the validity of all form fields and applies a style to each field. If the form should only apply custom styles to a subset of the fields, make CustomFieldClassProvider apply styles conditionally. The following CustomFieldClassProvider2 example only applies a style to the Name field. For any fields with names not matching Name, string.Empty is returned, and no style is applied.

CustomFieldClassProvider2.cs:

using System.Linq;
using Microsoft.AspNetCore.Components.Forms;

public class CustomFieldClassProvider2 : FieldCssClassProvider
{
    public override string GetFieldCssClass(EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        if (fieldIdentifier.FieldName == "Name")
        {
            var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

            return isValid ? "validField" : "invalidField";
        }

        return string.Empty;
    }
}

Add an additional property to ExampleModel, for example:

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

Add the Description to the ExampleForm7 component's form:

<InputText id="description" @bind-Value="exampleModel.Description" />

Update the EditContext instance in the component's OnInitialized method to use the new Field CSS Class Provider:

editContext.SetFieldCssClassProvider(new CustomFieldClassProvider2());

Because a CSS validation class isn't applied to the Description field (id="description"), it isn't styled. However, field validation runs normally. If more than 10 characters are provided, the validation summary indicates the error:

Description is too long.

In the following example:

  • The custom CSS style is applied to the Name field.

  • Any other fields apply logic similar to Blazor's default logic and using Blazor's default field CSS validation styles, modified with valid or invalid. Note that for the default styles, you don't need to add them to the app's stylesheet if the app is based on a Blazor project template. For apps not based on a Blazor project template, the default styles can be added to the app's stylesheet:

    .valid.modified:not([type=checkbox]) {
        outline: 1px solid #26b050;
    }
    
    .invalid {
        outline: 1px solid red;
    }
    

CustomFieldClassProvider3.cs:

using System.Linq;
using Microsoft.AspNetCore.Components.Forms;

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

        if (fieldIdentifier.FieldName == "Name")
        {
            return isValid ? "validField" : "invalidField";
        }
        else
        {
            if (editContext.IsModified(fieldIdentifier))
            {
                return isValid ? "modified valid" : "modified invalid";
            }
            else
            {
                return isValid ? "valid" : "invalid";
            }
        }
    }
}

Update the EditContext instance in the component's OnInitialized method to use the preceding Field CSS Class Provider:

editContext.SetFieldCssClassProvider(new CustomFieldClassProvider3());

Using CustomFieldClassProvider3:

  • The Name field uses the app's custom validation CSS styles.
  • The Description field uses logic similar to Blazor's logic and Blazor's default field CSS validation styles.

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.

Warning

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. Experimental features are provided for the purpose of exploring feature viability and may not ship in a stable version. 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();

    ...
}
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; }
}
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, the following example:

  • Uses a shortened version of the preceding Starfleet Starship Database form (FormExample2 component) that only accepts a value for the ship's identifier. The other Starship properties receive valid default values when an instance of the Starship type is created.
  • Uses the form's EditContext to assign the model when the component is initialized.
  • Validates the form in the context's OnFieldChanged callback to enable and disable the submit button.
  • Implements IDisposable and unsubscribes the event handler in the Dispose method. For more information, see ASP.NET Core Razor component lifecycle.

Note

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

Pages/FormExample9.razor:

@page "/form-example-9"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample9> Logger

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

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>

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

@code {
    private Starship starship = 
        new()
        {
            Identifier = "NCC-1701",
            Classification = "Exploration",
            MaximumAccommodation = 150,
            IsValidatedDesign = true,
            ProductionDate = new DateTime(2245, 4, 11)
        };
    private bool formInvalid = false;
    private EditContext? editContext;

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

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

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}
@page "/form-example-9"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample9> Logger

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

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>

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

@code {
    private Starship starship = 
        new()
        {
            Identifier = "NCC-1701",
            Classification = "Exploration",
            MaximumAccommodation = 150,
            IsValidatedDesign = true,
            ProductionDate = new DateTime(2245, 4, 11)
        };
    private bool formInvalid = false;
    private EditContext? editContext;

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

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

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }

    public void Dispose()
    {
        if (editContext is not null)
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}
@page "/form-example-9"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample9> Logger

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

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>

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

@code {
    private Starship starship = 
        new()
        {
            Identifier = "NCC-1701",
            Classification = "Exploration",
            MaximumAccommodation = 150,
            IsValidatedDesign = true,
            ProductionDate = new DateTime(2245, 4, 11)
        };
    private bool formInvalid = false;
    private EditContext editContext;

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

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

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }

    public void Dispose()
    {
        editContext.OnFieldChanged -= HandleFieldChanged;
    }
}
@page "/form-example-9"
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<FormExample9> Logger

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

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="starship.Identifier" />
        </label>
    </p>

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

@code {
    private Starship starship = 
        new Starship()
        {
            Identifier = "NCC-1701",
            Classification = "Exploration",
            MaximumAccommodation = 150,
            IsValidatedDesign = true,
            ProductionDate = new DateTime(2245, 4, 11)
        };
    private bool formInvalid = false;
    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();
    }

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        // Process the valid form
    }

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

If a form isn't preloaded with valid values and you wish to disable the Submit button on form load, set formInvalid to true.

A side effect of the preceding approach is that a validation summary (ValidationSummary component) is populated with invalid fields after the user interacts with any one field. Address this scenario 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 assigns a Model or an EditContext. Don't use both for the same form.

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

private ExampleModel exampleModel = new();
private ExampleModel exampleModel = new ExampleModel();

Additional resources