Validate Your Blazor Form Using EditForm

The server-side Blazor will be released with ASPNET Core 3. With this release, the ASP.NET team worked on implementing form validation so anyone could implement their own validation logic and the framework would take care of the rest, such as blocking form submit, adding/removing the CSS class, and displaying error message etc. They have also implemented the first validator for classes annotated with DataAnnotation attribute.

Most of this work is implemented by this PR which was merged 6 weeks ago. This code was made available on Blazor 0.9 one month ago.

I already wrote my own form validation logic but their solution is way better as it requires less plumbing: you add the model reference only once (at the form level); then all the child components will know about it via the EditContext.

Blazor form validation component

Form validation is implemented mostly on the namespace “Microsoft.AspNetCore.Components.Forms”. The source code is located here (Components will be renamed back to Blazor before the 3.0 release). The main classes, I think, you should know about are :

  • AspNetCore.Components.Forms.EditContext 
    This class group the validation information (validator, message, fields) for one model instance. This is the integration point of your custom validation, by subscribing to its event, you can execute your own validation logic and send your error to the GUI.

  • AspNetCore.Components.Forms.EditForm 
    This component is an HTML form tag that’ll instantiate the EditContext for a given model instance. It also provides an event for submitting your form only when the validation succeeds or handling when validation fails.

  • AspNetCore.Components.Forms.ValidationMessageStore 
    This class is used for adding error about a field to an EditContext.

Here is how the validation is executed :

  • The EditForm instantiate the EditContext with the model instance you gave it.
  • Services are created by you or some framework components and listen to the EditContext event, they have to create a ValidationMessageStore for making errors available to the EditContext.
  • When the form is submited, EditForm calls Validate on the EditContext
  • EditContext triggers the event OnValidationRequested with itself as a parameter
  • Every service who is listening to this instance event will do their validation work and push error to the message store.
  • When you push an error to the message store, it creates a field reference on the EditContext, links itself to this field (internal class FieldState), and stores the error message on a map.
  • When every listener has done its job, the EditContext browses all the fields states and checks if there is any error. If there is one error, then the submit callback is not called.

I don’t know if I am clear enough here; I hope it’ll be clearer by the end of this post.

Validate your form

Here is a registration form validated via the data annotation attributes.

  1. <EditForm  OnValidSubmit="CreateAccount" Model="@registerCommand">  
  2.      <DataAnnotationsValidator />  
  3.      <div asp-validation-summary="All" class="text-danger"></div>  
  4.      <div class="form-group row mb-1">  
  5.          <label class="col-sm-3 col-form-label" for="NewEmail">Email</label>  
  6.   
  7.          <div class="col-sm-9">  
  8.              <InputText Class="form-control" bind-Value="@registerCommand.Email" />  
  9.              <ValidationMessage For="@(() => registerCommand.Email)"/>  
  10.          </div>  
  11.   
  12.      </div>  
  13.      <div class="form-group row mb-1">  
  14.          <label class="col-sm-3 col-form-label" for="NewName">Name</label>  
  15.   
  16.          <div class="col-sm-9">  
  17.              <InputText Class="form-control" bind-Value="@registerCommand.Name" />  
  18.               <ValidationMessage For="@(() => registerCommand.Name)" />  
  19.          </div>  
  20.      </div>  
  21.      <div class="form-group row mb-1">  
  22.          <label class="col-sm-3 col-form-label" for="NewPassword">Password</label>  
  23.   
  24.          <div class="col-sm-9">  
  25.              <InputPassword Class="form-control" bind-Value="@registerCommand.Password" />  
  26.               <ValidationMessage For="@(() => registerCommand.Password)" />  
  27.          </div>  
  28.      </div>  
  29.      <div class="form-group row mb-1">  
  30.          <label class="col-sm-3 col-form-label" for="NewConfirmPassword">Confirm</label>  
  31.   
  32.          <div class="col-sm-9">  
  33.              <InputPassword Class="form-control" bind-Value="@registerCommand.ConfirmPassword" />  
  34.              <ValidationMessage For="@(() => registerCommand.ConfirmPassword)" />  
  35.          </div>  
  36.      </div>  
  37.      <div class="form-group text-center mb-0">  
  38.          <button type="submit" ref="createButton" id="BtnRegister" class="btn btn-primary">Register</button>  
  39.         
  40.      </div>    
  41.  </EditForm>  
  • I used EditorForm instead of plain HTML form. It’ll provide all the validation logic and needed service.
  • InputText is used for binding your input to the validation logic that will be executed when you edit the value. The “invalid” CSS class will be added if the field is invalid; “valid” will be added if it’s not.
  • ValidationMessage displays the error message for the given field in a div with the class “validation-message”. You also have a ValidationSUmmary if you want to display all your message on the same place.
  • I found a bug the way it handles the CompareAttribute, I will try to fix this and send a PR.
  • InputPassword is my own, as the ASPNET Team decided to provide only a limited set of input attribute via the build-in components. It’s not a big problem because creating this component is as simple as this,
  1. @inherits InputBase<string>  
  2. <input bind="@CurrentValue" type="password" id="@Id" class="@CssClass" />  
  3.   
  4. @functions{  
  5.         protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage)  
  6.         {  
  7.             result = value;  
  8.             validationErrorMessage = null;  
  9.             return true;  
  10.         }  
  11. }  

Maybe when something like Angular decorator is available in Blazor, it’ll be simpler but so far, it’s not a big deal.

I also added the following CSS for applying Bootstrap styling to the errors.

  1. .form-control.invalid{  
  2.     border-color:#dc3545;  
  3. }  
  4. .form-control.valid{  
  5.     border-color:#28a745;  
  6. }  
  7. .validation-message {  
  8.     width: 100%;  
  9.     margin-top: .25rem;  
  10.     font-size: 80%;  
  11.     color: #dc3545;  
  12. }  

Now, when submitting the form or changing an input value, the fields are red and the error messages are displayed like this.

Validate Your Blazor Form Using The EditForm 

Display validation error from the server

Personally, I don’t like to handle validation about the global state (like the uniqueness of an email) with validation attribute, I prefer to handle it explicitly on my command handler. So it can happen that my server returns validation error. For returning those errors from the server I simply build a Dictionary<string, List<string>> where the key is the field name and the values are the error message from the server and return it with a bad request (400) Http status. You can checkout my project Toss how I do it on the server side here.

On the client side, I first have to plug my custom valdiator to the EditContext. This is my validator,

  1. public class ServerSideValidator : ComponentBase  
  2. {  
  3.     private ValidationMessageStore _messageStore;  
  4.   
  5.     [CascadingParameter] EditContext CurrentEditContext { get; set; }  
  6.   
  7.     /// <inheritdoc />  
  8.     protected override void OnInit()  
  9.     {  
  10.         if (CurrentEditContext == null)  
  11.         {  
  12.             throw new InvalidOperationException($"{nameof(ServerSideValidator)} requires a cascading " +  
  13.                 $"parameter of type {nameof(EditContext)}. For example, you can use {nameof(ServerSideValidator)} " +  
  14.                 $"inside an {nameof(EditForm)}.");  
  15.         }  
  16.   
  17.         _messageStore = new ValidationMessageStore(CurrentEditContext);  
  18.         CurrentEditContext.OnValidationRequested += (s, e) => _messageStore.Clear();  
  19.         CurrentEditContext.OnFieldChanged += (s, e) => _messageStore.Clear(e.FieldIdentifier);  
  20.     }  
  21.   
  22.     public void DisplayErrors(Dictionary<string, List<string>> errors)  
  23.     {  
  24.         foreach (var err in errors)  
  25.         {  
  26.             _messageStore.AddRange(CurrentEditContext.Field(err.Key), err.Value);  
  27.         }          
  28.         CurrentEditContext.NotifyValidationStateChanged();  
  29.     }  
  30. }  
  • It’s highly inspired by the DataAnnotationValidator
  • It’s a component as it’ll have to be inserted on the component hierarchy for getting the cascading EditContext from the form
  • As said before, I have to create a ValidationMessageStore for pushing errors to the context
  • I cleaned the error when a field is edited so the user can retry another value

For using this, I have to add this component under my form like this.

  1.    <EditForm  OnValidSubmit="CreateAccount" Model="@registerCommand" ref="registerForm">  
  2.         <DataAnnotationsValidator />  
  3.         <ServerSideValidator ref="serverSideValidator"/>  
  4.         ....  
  5.     </EditForm>  
  6.     ...  
  7.   
  8. @functions{  
  9.     RegisterCommand registerCommand = new RegisterCommand();  
  10.     ServerSideValidator serverSideValidator;  
  11.     async Task CreateAccount(EditContext context)  
  12.     {  
  13.         await ClientFactory.Create("/api/account/register", createButton)  
  14.             .OnBadRequest<Dictionary<string, List<string>>>(errors => {  
  15.   
  16.                 serverSideValidator.DisplayErrors(errors);  
  17.             })  
  18.             .OnOK(async () =>  
  19.             {  
  20.                 await JsInterop.Toastr("success""Successfully registered, please confirm your account by clicking on the link in the email sent to " + registerCommand.Email);  
  21.                 registerCommand = new RegisterCommand();  
  22.                 StateHasChanged();  
  23.             })  
  24.             .Post(registerCommand);  
  25.     }  
  26. }  
  • I used the “ref” keyword for interacting directly with the validator
  • The field name pushed by the server must match the field name of the command
  • With this if the user name or email are not unique, the message will be displayed beneath the good field.

Conclusion

I don’t know if this is the best way to do something like that but it works and it’s not very complicated. The approach taken by the ASPNET Team is quite good as you don’t have to implement an adapter interface, you just have to listen to the event you want to use. And they are going on the model validation path rather than the reactive form way the angular team took, which is IMHO way better.

The good thing here, just like with the first blog post, is that I don’t have to implement twice some validation mechanism: I just add my Data Annotation attributes to my command/query class and they’ll be validated on both the client and the server.

The next step might be to implement custom validators using services that would work on both sides.


Similar Articles