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 data annotation 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 groups 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 an error about a field to an EditContext.
Here is how the validation is executed.
- The EditForm instantiates 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 submitted, 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 its validation work and push the 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 field 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.
<EditForm OnValidSubmit="CreateAccount" Model="@registerCommand">
<DataAnnotationsValidator />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label" for="NewEmail">Email</label>
<div class="col-sm-9">
<InputText Class="form-control" bind-Value="@registerCommand.Email" />
<ValidationMessage For="@(() => registerCommand.Email)" />
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label" for="NewName">Name</label>
<div class="col-sm-9">
<InputText Class="form-control" bind-Value="@registerCommand.Name" />
<ValidationMessage For="@(() => registerCommand.Name)" />
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label" for="NewPassword">Password</label>
<div class="col-sm-9">
<InputPassword Class="form-control" bind-Value="@registerCommand.Password" />
<ValidationMessage For="@(() => registerCommand.Password)" />
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label" for="NewConfirmPassword">Confirm</label>
<div class="col-sm-9">
<InputPassword Class="form-control" bind-Value="@registerCommand.ConfirmPassword" />
<ValidationMessage For="@(() => registerCommand.ConfirmPassword)" />
</div>
</div>
<div class="form-group text-center mb-0">
<button type="submit" ref="createButton" id="BtnRegister" class="btn btn-primary">Register</button>
</div>
</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 Validation Summary if you want to display all your messages in the same place.
- I found a bug in 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 attributes via the built-in components. It’s not a big problem because creating this component is as simple as this,
@inherits InputBase<string>
<input bind="@CurrentValue" type="password" id="@Id" class="@CssClass" />
@functions{
protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage)
{
result = value;
validationErrorMessage = null;
return true;
}
}
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.
.form-control.invalid {
border-color: #dc3545;
}
.form-control.valid {
border-color: #28a745;
}
.validation-message {
width: 100%;
margin-top: .25rem;
font-size: 80%;
color: #dc3545;
}
Now, when submitting the form or changing an input value, the fields are red and the error messages are displayed like this.
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 a 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 validator to the EditContext. This is my validator.
public class ServerSideValidator : ComponentBase
{
private ValidationMessageStore _messageStore;
[CascadingParameter]
EditContext CurrentEditContext { get; set; }
/// <inheritdoc />
protected override void OnInit()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(ServerSideValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(ServerSideValidator)} " +
$"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.AddRange(CurrentEditContext.Field(err.Key), err.Value);
}
CurrentEditContext.NotifyValidationStateChanged();
}
}
- It’s highly inspired by the DataAnnotationValidator
- It’s a component as it’ll have to be inserted into the component hierarchy to get 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
To use this, I have to add this component under my form like this.
<EditForm OnValidSubmit="CreateAccount" Model="@registerCommand" ref="registerForm">
<DataAnnotationsValidator />
<ServerSideValidator ref="serverSideValidator" />
<!-- Other form elements -->
</EditForm>
<!-- Other content -->
@functions{
RegisterCommand registerCommand = new RegisterCommand();
ServerSideValidator serverSideValidator;
async Task CreateAccount(EditContext context)
{
await ClientFactory.Create("/api/account/register", createButton)
.OnBadRequest<Dictionary<string, List<string>>>(errors => {
serverSideValidator.DisplayErrors(errors);
})
.OnOK(async () => {
await JsInterop.Toastr("success", "Successfully registered, please confirm your account by clicking on the link in the email sent to " + registerCommand.Email);
registerCommand = new RegisterCommand();
StateHasChanged();
})
.Post(registerCommand);
}
}
- 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 is 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.