Advanced Validation with Blazor EditForm

Happy New Year 🎉

Blazor has a built-in form-handling framework based on the EditForm component. This framework supports validation for user-entered data before allowing the form to be submitted. I won’t go into the details of using EditForm, as Microsoft’s documentation already does an excellent job covering that.

One of this framework’s most widely used components is the InputText component. This renders a standard HTML input element but is hooked into EditContext of the form-handling framework. This works very well when you only deal with text input, but sometimes, you must validate something other than text. Other components, such as InputDate, InputNumber etc., map to different HTML input elements with varying type attributes. However, if you need to validate data for something else, such as a GUID or a custom type and still support user-entered data within a normal input element, this is not possible “out-of-the-box”.

Luckily, the framework does come with data annotation validation through the  DataAnnotationsValidator component. Using this, we can apply custom validation attributes to our model.

Let’s imagine a scenario where we want to add a new user to our system, but the ID for the user needs to be provided and must be a GUID. We can use the following form, which uses everything that comes by default with Blazor.

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

<PageTitle>Home</PageTitle>

<h1>Advanced Validation with EditForm</h1>

<EditForm EditContext="@EditContext" OnValidSubmit="@OnValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div class="form-group">
        <label>ID:</label>
        <InputText @bind-Value="User.Id" class="form-control" />
        <ValidationMessage For="@(() => User.Id)" />
        
    </div>
    <div class="form-group">
        <label>First Name:</label>
        <InputText @bind-Value="User.FirstName" class="form-control" />
        <ValidationMessage For="@(() => User.FirstName)" />
    </div>
    <div class="form-group">
        <label>Last Name:</label>
        <InputText @bind-Value="User.LastName" class="form-control" />
        <ValidationMessage For="@(() => User.LastName)" />
    </div>
    <div class="form-group">
        <label>Age:</label>
        <InputNumber @bind-Value="User.Age" class="form-control" />
        <ValidationMessage For="@(() => User.Age)" />
    </div>
    <button class="btn btn-primary" type="submit">Submit</button> 
</EditForm>

@code {
    UserModel? User { get; set; }
    public EditContext? EditContext { get; set; }

    public void OnValidSubmit()
    {
        Console.WriteLine("OnValidSubmit called");
    }

    protected override void OnInitialized()
    {
        User = new UserModel();
        EditContext = new EditContext(User);
    }

    public class UserModel
    {
        [Required]
        public string? Id { get; set; }

        [Required]
        public string? FirstName { get; set; }

        [Required]
        public string? LastName { get; set; }

        [Required]
        [Range(0, 120)]
        public int? Age { get; set; }
    }
}

This code presents a form that requests the user’s ID, First Name, Last Name and Age. It also validates that all required data is provided and within reasonable values.

Well, almost – we know that the ID must be a GUID, but we are not validating that. We also cannot change the property in the model to be a GUID because the InputText will not bind to that.

The solution is to create a custom validation attribute that can be applied to the ID property. This will perform the required validation while allowing the property to remain a string to work with the InputText component.

public sealed class IsParsableToTypeAttribute<T>() : ValidationAttribute(DefaultErrorMessage) where T : IParsable<T>
{
    private static readonly string DefaultErrorMessage = "The {0} field is not parsable to type '" + typeof(T).FullName + "'";

    public override bool IsValid(object value)
    {
        the return value is string valueAsString
               && T.TryParse(valueAsString, null, out _);
    }
}

This attribute uses generics with any custom type that implements the IParsable interface. It checks whether the supplied value is a string and, if so, checks whether the value can be successfully parsed.

Finally, we need to apply this attribute to the Id property.

[Required]
[IsParsableToType<Guid>]
public string? Id { get; set; }

When we run this, we get validation that the field must be a GUID.

When setting a valid GUID, validation once again passes.

The complete code for this post is available on GitHub.

I hope you have a wonderful 2024 🌟

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.