Blazor form validation

Client-side Blazor supports DataAnnotations form validation out of box. It’s simple and intuitive but also very flexible – if needed we can use the same mechanism to replace DataAnnotations validation with some other validation component. This blog post introduces form validation in Blazor applications and peeks also into engine of validation mechanism.

NB! Form validation in Blazor is experimental and subject to changes. This blog post is written using .NET Core 3.0 Preview 7.

Data annotations validation

Blazor supports DataAnnotations validation out-of-box. To use validation we have to have model with data annotations and edit form defined in Blazor view.

Let’s create simple model for guestbook. It has properties for poster name and message.

public class GuestbookEntry
{
    [Required]
    [MaxLength(10)]
    public string Name { get; set; }

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

Here is Blazor form that uses out-of-box form validation. I followed same pattern with validation messages that should be familiar at least those who know ASP.NET MVC.

@page "/"
@using  BlazorFormValidation.Models

<h1>My guestbook</h1>

<p>Leave me a message if you like my site</p>

<EditForm Model="@Model" OnValidSubmit="@HandleValidSubmit" OnInvalidSubmit="@HandleInvalidSubmit">
    <div class="alert @StatusClass">@StatusMessage</div>
   
    <DataAnnotationsValidator />
    <ValidationSummary />
   
    <div class="form-group">
        <label for="name">Name: </label>
        <InputText Id="name" Class="form-control" @bind-Value="@Model.Name"></InputText>
        <ValidationMessage For="@(() => Model.Name)" />
    </div>
    <div class="form-group">
        <label for="body">Text: </label>
        <InputTextArea Id="body" Class="form-control" @bind-Value="@Model.Text"></InputTextArea>
        <ValidationMessage For="@(() => Model.Text)" />
    </div>
    <button type="submit">Ok</button>

</EditForm>

@code 
{
    private string StatusMessage;
    private string StatusClass;

    private GuestbookEntry Model = new GuestbookEntry();

    protected void HandleValidSubmit()
    {
        StatusClass = "alert-info";
        StatusMessage = DateTime.Now + " Handle valid submit";
    }

    protected void HandleInvalidSubmit()
    {
        StatusClass = "alert-danger";
        StatusMessage = DateTime.Now + " Handle invalid submit";
    }
}

EditForm is Blazor container for forms. Validator is added like any other Blazor component. Page above uses DataAnnotationValidator but you can create custom one if you like.

Here is the screenshot demonstrating form when fields are empty and Ok button is clicked.

Blazor guestbook form is invalid

This is the form with successful validation.

Blazor guestbook form is valid

I think most of developers will be okay with this solution and I can finish this writing. Or no?

Disable button until form is valid

We can actually dig deeper and try something fancy. What if we want Ok button to be disabled while form is invalid? I found blog post Disabling the Submit button in Blazor Validation by Peter Himschoot where he provider solution through custom InputWatcher.

My solution is smaller. We can assign to EditForm either Model or EditContext but not both at same time. When using EditContext we have way more control over validation. This way I solved the need for additional component.

I solved button disabled status problem with simple but not so straightforward trick. Instead of boolean true-false value for disabled attribute I went with string. Value is either “disabled” or null. Notice that empty string leaves “disabled” attribute on button. I had to listen to field change event just like Peter did.

@page "/"
@using  BlazorFormValidation.Models

<h1>My guestbook</h1>

<p>Leave me a message if you like my site</p>

<EditForm EditContext="@EditContext">  
    <DataAnnotationsValidator />
   
    <div class="form-group">
        <label for="name">Name: </label>
        <InputText Id="name" Class="form-control" @bind-Value="@Model.Name"></InputText>
        <ValidationMessage For="@(() => Model.Name)" />
    </div>
    <div class="form-group">
        <label for="body">Text: </label>
        <InputTextArea Id="body" Class="form-control" @bind-Value="@Model.Text"></InputTextArea>
        <ValidationMessage For="@(() => Model.Text)" />
    </div>
    <button type="submit" disabled="@OkayDisabled">Ok</button>

</EditForm>

@code 
{
    private EditContext EditContext;
    private GuestbookEntry Model = new GuestbookEntry();

    protected string OkayDisabled { get; set; } = "disabled";

    protected override void OnInit()
    {
        EditContext = new EditContext(Model);
        EditContext.OnFieldChanged += EditContext_OnFieldChanged;

        base.OnInit();
    }

    protected override void OnAfterRender()
    {
        base.OnAfterRender();

        SetOkDisabledStatus();
    }

    private void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs e)
    {
        SetOkDisabledStatus();
    }

    private void SetOkDisabledStatus()
    {
        if(EditContext.Validate())
        {
            OkayDisabled = null;
        }
        else
        {
            OkayDisabled = "disabled";
        }
    }
}

This is how guestbook opens when I run it. Validation messages are shown for fields and Ok button is disabled.

Blazor guestbook: button is disabled if form is invalid

With EditContext class we were able to take form under our control.

Inside EditContext class

There’s more about EditContext. Take a look at EditContext class definiton.

public sealed class EditContext
{
    public EditContext(object model);
    public object Model { get; }

    public event EventHandler<FieldChangedEventArgs> OnFieldChanged;
    public event EventHandler<ValidationRequestedEventArgs> OnValidationRequested;
    public event EventHandler<ValidationStateChangedEventArgs> OnValidationStateChanged;

    public FieldIdentifier Field(string fieldName);
    public IEnumerable<string> GetValidationMessages();
    public IEnumerable<string> GetValidationMessages(FieldIdentifier fieldIdentifier);
    public bool IsModified();
    public bool IsModified(in FieldIdentifier fieldIdentifier);
    public void MarkAsUnmodified(in FieldIdentifier fieldIdentifier);
    public void MarkAsUnmodified();
    public void NotifyFieldChanged(in FieldIdentifier fieldIdentifier);
    public void NotifyValidationStateChanged();
    public bool Validate();
}

It has events that are triggered for changes. We have methods to mark fields as unmodified, it’s easy to get field ID used bu Blazor and notify EditContext of changes if we need.

Check validation status when field changes

There’s one more minor thing to solve on our form. When form gets valid I want Ok button to be enabled at same moment and not when I leave the field. This functionality would be great for fields validated by regular expression, by example.

After some research and hacking I came out with ugly solution using KeyUp event and reflection (I’m sure Blazor guys don’t want you to do it at home). But here my solution is.

@page "/"
@using  BlazorFormValidation.Models

<h1>My guestbook</h1>

<p>Leave me a message if you like my site</p>

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

    <div class="form-group">
        <label for="name">Name: </label>
        <InputText Id="name" Class="form-control" @bind-Value="@Model.Name" onkeyup='@(e => KeyUp(e, "Name"))'></InputText>
        <ValidationMessage For="@(() => Model.Name)" />
    </div>
    <div class="form-group">
        <label for="body">Text: </label>
        <InputTextArea Id="body" Class="form-control" @bind-Value="@Model.Text" onkeyup='@(e => KeyUp(e, "Text"))'></InputTextArea>
        <ValidationMessage For="@(() => Model.Text)" />
    </div>
    <button type="submit" disabled="@OkayDisabled">Ok</button>

</EditForm>

@code
{
    private EditContext EditContext;
    private GuestbookEntry Model = new GuestbookEntry();

    protected string OkayDisabled { get; set; } = "disabled";

    protected override void OnInit()
    {
        EditContext = new EditContext(Model);
        EditContext.OnFieldChanged += EditContext_OnFieldChanged;

        base.OnInit();
    }

    protected override void OnAfterRender()
    {
        base.OnAfterRender();

        SetOkDisabledStatus();
    }

    private void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs e)
    {
        SetOkDisabledStatus();
    }

    void KeyUp(UIKeyboardEventArgs e, string memberName)
    {
        var property = Model.GetType().GetProperty(memberName);
        var value = property.GetValue(Model);
        property.SetValue(Model, value + e.Key);

        var id = EditContext.Field(memberName);

        EditContext.NotifyFieldChanged(id);
    }

    private void SetOkDisabledStatus()
    {
        if (EditContext.Validate())
        {
            OkayDisabled = null;
        }
        else
        {
            OkayDisabled = "disabled";
        }
    }
}

It’s a small thing but users will love it. We just made our form even more responsive.

Using other validators

I found FluentValidation support for Blazor by Chris Sainty. FluentValidation is popular validation library that supports also advanced validation scenarios.

With FluentValidation we use validator classes like the one shown here.

public class GuestbookEntryValidator : AbstractValidator<GuestbookEntry>
{
    public GuestbookEntryValidator()
    {
        RuleFor(e => e.Name)
                .NotEmpty().WithMessage("Name is required")
                .MaximumLength(10).WithMessage("10 characters maximum");

        RuleFor(e => e.Text)
                .NotEmpty().WithMessage("Text is required");
    }
}

Our form doesn’t change much. We just have to change validator component.

<EditForm EditContext="@EditContext">
    <FluentValidationValidator />

    <div class="form-group">
        <label for="name">Name: </label>
        <InputText Id="name" Class="form-control" @bind-Value="@Model.Name"></InputText>
        <ValidationMessage For="@(() => Model.Name)" />
    </div>
    <div class="form-group">
        <label for="body">Text: </label>
        <InputTextArea Id="body" Class="form-control" @bind-Value="@Model.Text"></InputTextArea>
        <ValidationMessage For="@(() => Model.Text)" />
    </div>
    <button type="submit" disabled="@OkayDisabled">Ok</button>

</EditForm>

Now we can use full power of FluentValidator on our forms. Check out FluentValidationValidator souce code from Github repository chrissainty/FluentValidationWithRazorComponents.

Wrapping up

There’s two ways how to go with validation on Blazor. We can use Model property of EditForm and keep things simple for us. If we need more control over validation and UI then we can provide EditContext to EditForm. Those who want to go deep to internals can see how Christ Sainty integrated FluentValidation to Blazor. Although there can be changes to form validation over coming releases of Blazor we can still start building our own advanced components to use with Blazor applications.

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    Leave a Reply

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