Validating Blazor Forms

When you allow users to provide values that you then process, you need to ensure that the incoming values are of the expected data type, that they are within the permitted range and that required values are present according to your application's business rules. This process is known as input validation.

The term "user input" covers any value that the user has control over. Values provided via forms constitute the bulk of user input, but user input also comes in the form of values provided in URLs and cookies. The default position should be that all user input is to be considered untrusted and should be validated against business rules. Here, we concentrate our focus on validating form values.

You can perform validation on form data in two places in a web application: in the browser using either client-side code or the browser's in-built data type validation; and on the server using C# code. However, you should only ever view client-side validation as a courtesy to the user because it is easily circumnavigated by anyone who knows how to use the browser's developer tools for example. Server side validation should be seen as essential.

On the server, it is the API's responsibility to validate incoming data. In Blazor WASM, form validation takes place on the client.

Validation using DataAnnotation attributes

The Blazor input validation story is built around the EditContext, input validation components and a set of attributes that inherit from ValidationAttribute. Most of these attributes reside in the System.ComponentModel.DataAnnotations namespace. Each attribute is designed to perform a specific type of validation be it for presence, data type or range. Some also enable you to test the incoming value against an acceptable pattern.

The following table lists the validation attributes that you are most likely to use and the type of validation they provide, together with example usage.

Attribute Description
Compare Used to specify another property that the value should be compared to for equality [Compare(nameof(Password2))]
MaxLength Sets the maximum number of characters/bytes/items that can be accepted [MaxLength(20)]
MinLength Sets the minimum number of characters/bytes/items that can be accepted [MinLength(2)]
Range Sets the minimum and maximum values of a range [Range(5,8)], Range(typeof(DateTime),"2021-1-1","2021-12-31")]
RegularExpression Checks the value against the specified regular expression [RegularExpression(@"[a-zA-Z]+")]
Required Specifies that a value must be provided for this property. Non-nullable value types such as DateTime and numeric values are treated as required by default and do not need this attribute applied to them [Required]
StringLength Sets the maximum, and optionally, the minimum number of string characters allowed [StringLength(2)], [StringLength(10, MinimumLength=2)]

In addition, there are some data type validation attributes including Phone, EmailAddress, Url and CreditCard. These validate incoming values against predetermined formats to ensure that they are "well-formed". Documentation on what the attribute authors consider to be well-formed is sparse, but you can always resort to looking at the source code to see the logic used to test the incoming value to ensure that the implementation covers your business rules. The .NET Source Browser is a great tool for this purpose (https://source.dot.net/). Using that, or going directly to the source code for the EmailAddressAttribute, for example, will show you that email "validation" consists of little more than checking the presence of the "@" character in the input. The check ensures that there is only one instance of the character and it is not at the beginning or end of the input. So "a@b" will pass this validation test.

Basic Validation Example

The following InputModel class has four properties, all strings. They are all decorated with the Required attribute. The EmailAddress property will also be validated by the EmailAdress validator. The Password property has the MinLength attribute set to 8 and the Password2 property is decorated with the Compare attribute, which has the name of the Password field assigned to otherProperty parameter. Multiple attributes can be applied as separate declarations within their own square brackets (see the EmailAddress property), or as a comma separated list (Password property):

public class InputModel
{
    [Required]
    public string Name { get; set; }
    [Required]
    [EmailAddress]
    public string EmailAddress { get; set; }
    [Required, MinLength(8)]
    public string Password { get; set; }
    [Compare(nameof(InputModel.Password))]
    public string Password2 { get; set; }
}

An EditForm has an instance of the InputModel passed to its Model parameter. Each property has a corresponding input validation component (InputText) for capturing its data and a ValidationMessage component for displaying any validation error messages. The form also includes a DataAnnotationsValidator component. This is needed to enable automatic validation.

<EditForm Model="model">
    <DataAnnotationsValidator /> <!-- Required for validation -->
    <div class="mb-3">
        <label for="Name" class="form-label">Name</label>
        <InputText @bind-Value=model.Name class="form-control" /> <!-- Input validation component -->
        <ValidationMessage For="() => model.Name" /> <!-- Display validation messages for this property -->
    </div>
    <div class="mb-3">
        <label for="EmailAddress" class="form-label">Email Address</label>
        <InputText @bind-Value=model.EmailAddress class="form-control" />
        <ValidationMessage For="() => model.EmailAddress" />
    </div>
    <div class="mb-3">
        <label for="Password" class="form-label">Password</label>
        <InputText @bind-Value=model.Password class="form-control" />
        <ValidationMessage For="() => model.Password" />
    </div>
    <div class="mb-3">
        <label for="Password2" class="form-label">Enter your password again</label>
        <InputText @bind-Value=model.Password2 class="form-control" />
        <ValidationMessage For="() => model.Password2" />
    </div>
    <div class="mb-3">
        <button class="btn btn-primary">Submit</button> <!-- Validation initially fires when form submitted -->
    </div>
</EditForm>
</div>
@code {
    InputModel model = new();
}

The form is initially validated when it is submitted. Default validation error messages are displayed within a div element generated by the ValidationMessage component. The div has a CSS class of validation-message applied to it. The inputs have CSS classes applied to them - either valid or invalid. Styles for these classes are defined in the template version of app.css and can be modified easily.

Blazor Form Validation

You can customise the error message itself via the validation attribute's ErrorMessage property:

[Required(ErrorMessage="Please provide your name")]
public string Name { get; set; }

The validation status of each field is updated when the value is changed. Once a value has been changed, another CSS class, modified, is applied to its rendered form control.

Blazor Form Validation

A summary of all validation messages can be presented in one place instead of, or in addition to those for individual properties by adding a ValidationSummary component to the form where you would like the validation errors to be displayed in an unordered list:

<EditForm Model="model">
    <DataAnnotationsValidator />
    <ValidationSummary />

    //... 

</EditForm>

Blazor Form Validation

The ul element has a CSS class of validation-errors applied to it and individual error messages get the same validation-message class as the ValidationMessage component:

CSS classes for validation messages in Blazor

Manual Validation

The EditForm component exposes a number of events that fire when a form is submitted that you can hook into by passing an EventCallback to the corresponding parameter:

  • OnSubmit - fires for every form submission but does not validate the form
  • OnValidSubmit - fires only if there are no validation errors
  • OnInvalidSubmit - fires only if there are validation errors

When the form is submitted, a check is made to establish if a delegate has been specified for the OnSubmit event. If it has, the delegate is invoked. If not, the form is validated, and depending on the result, either the OnValidSubmit or OnInvalidSubmit event is fired.

The OnSubmit event is useful if you want to add some custom validation. For example, you might want to validate a field only in some circumstances. Imagine that you want to capture a postal address. You provide a field for a postal code, but this only applies to addresses in countries that operate a post code system. When the user selects such a country, the post code field is required, otherwise it isn't.

The following example shows two classes - PostalAddress and Country. The StreetAddress property within the PostalAddress class is required:

public class PostalAddress
{
    [Required]
    public string StreetAddress { get; set; }
    public string AddressLocality { get; set; }
    public string AddressRegion { get; set; }
    public string PostalCode { get; set; }
    public string AddressCountry { get; set; }
}

public class Country
{
    public string Alpha2 { get; set; }
    public string Name { get; set; }
    public bool HasPostalCodes { get; set; }
}

A collection of countries is created, some of which have postal codes while others do not:

List<Country> Countries { get; set; } = new List<Country>()
{
    new Country{ Alpha2 = "BW", Name="Botswana", HasPostalCodes = false },
    new Country{ Alpha2 = "TD", Name="Chad", HasPostalCodes = false },
    new Country{ Alpha2 = "IE", Name="Ireland", HasPostalCodes = false },
    new Country{ Alpha2 = "JM", Name="Jamaica ", HasPostalCodes = false },
    new Country{ Alpha2 = "KW", Name="Kuwait", HasPostalCodes = true },
    new Country{ Alpha2 = "MX", Name="Mexico", HasPostalCodes = true },
    new Country{ Alpha2 = "PH", Name="Philippines", HasPostalCodes = true },
    new Country{ Alpha2 = "SE", Name="Sweden", HasPostalCodes = true },
    new Country{ Alpha2 = "GB", Name="United Kingdom", HasPostalCodes = true },
    new Country{ Alpha2 = "US", Name="USA", HasPostalCodes = true }
};

These are used to populated an InputSelect component in a form that takes an EditContext and includes a DataAnnotationsValidator component. It also has an event handler specified for the OnSubmit event:

<EditForm EditContext="editContext" OnSubmit="HandleSubmit">
    <DataAnnotationsValidator/>
    <div class="form-group">
        <label class="form-label">Street</label>
        <InputText @bind-Value="Address.StreetAddress" class="form-control" />
        <ValidationMessage For="() => Address.StreetAddress" class="text-danger d-block" />
    </div>
    <div class="form-group">
        <label class="form-label">Locality</label>
        <InputText @bind-Value="Address.AddressLocality" class="form-control" />
        <ValidationMessage For="() => Address.AddressLocality" class="text-danger d-block" />
    </div>
    <div class="form-group">
        <label class="form-label">Region</label>
        <InputText @bind-Value="Address.AddressRegion" class="form-control" />
        <ValidationMessage For="() => Address.AddressRegion" class="text-danger d-block" />
    </div>
    <div class="form-group">
        <label class="form-label">Postal Code</label>
        <InputText @bind-Value="Address.PostalCode" class="form-control" />
        <ValidationMessage For="() => Address.PostalCode" class="text-danger d-block" />
    </div>
    <div class="form-group">
        <label class="form-label">Country</label>
        <InputSelect @bind-Value="Address.AddressCountry" class="form-select">
            <option value=""></option>
            @foreach (var country in Countries)
            {
                <option value="@country.Alpha2">@country.Name</option>
            }
        </InputSelect>
        <ValidationMessage For="() => Address.AddressCountry" class="text-danger d-block" />
    </div>
    <div class="form-group">
        <button class="btn btn-outline-primary mt-2">Submit</button>
    </div>
</EditForm>
@if(valid)
{
    <div class="alert alert-success mt-3">You submitted a valid form</div>
}

The component that contains the form has members for an EditContext, ValidationMessageStore and PostalAddress. They are brought together in the component's OnInitialized event:

EditContext editContext;
ValidationMessageStore validationMessages;
PostalAddress Address { get; set; } = new();
bool valid;

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

When the form is submitted, the HandleSubmit callback is executed. Within that, we must call the Validate method of the EditContext to manually validate the form. This causes any data annotations validation to execute. Then we check to see if the currently selected country operates a post code system, and if it does, whether a postal code has been provided. If not, we add a message to the validation message store. If a postal code has been provided, we remove any existing validation errors relating to the postal code field and revalidate the form which will result in the EditContext updating.

void HandleSubmit()
{
    valid = editContext.Validate();
    var postalCodeField = editContext.Field(nameof(PostalAddress.PostalCode));
    if (!string.IsNullOrEmpty(Address.AddressCountry))
    {
        var selectedCountry = Countries.First(x => x.Alpha2 == Address.AddressCountry);
        if (selectedCountry.HasPostalCodes)
        {
            if (string.IsNullOrWhiteSpace(Address.PostalCode))
            {
                validationMessages.Add(postalCodeField, "You must provide a postal code");
            }
            else
            {
                validationMessages.Clear(postalCodeField);
                valid = editContext.Validate();
            }
        }
    }
}
Last updated: 15/02/2023 09:00:50

Latest Updates

© 2023 - 2024 - Mike Brind.
All rights reserved.
Contact me at Outlook.com