Blazor Form Validation
Introducing form validation with Blazor and .NET Core 3.0.
Join the DZone community and get the full member experience.
Join For FreeClient-side Blazor supports DataAnnotations form validation out-of-the-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 looks at an engine of validation mechanism.
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-the-box. To use validation we have to have model with data annotations and edit form defined in Blazor view.
Let’s create a simple model for a 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 a Blazor form that uses out-of-the-box form validation. I followed the same pattern with validation messages that should be familiar, at least with 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 a Blazor container for forms. Validator is added like any other Blazor component. The page above uses DataAnnotationValidator, but you can create custom one if you like.
Here is the screenshot demonstrating form when fields are empty and the Ok button is clicked.
This is the form with successful validation.
I think most 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 an Ok button to be disabled while the form is invalid? I found a blog post Disabling the Submit button in Blazor Validation by Peter Himschoot where he provides a solution through custom InputWatcher.
My solution is smaller. We can assign to EditForm either Model or EditContext but not both at the same time. When using EditContext, we have way more control over validation. This way, I solved the need for additional components.
I solved the button-disabled-status problem with a simple but not so straightforward trick. Instead of a boolean value for the disabled attribute, I went with string. The value is either “disabled” or null. Notice that an empty string leaves “disabled” attribute on button. I had to listen to the 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 the Ok button is disabled.
With the EditContext class, we were able to take form under our control.
Inside EditContext Class
There’s more about EditContext. Take a look at the EditContext class definition.
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 by Blazor and notify EditContext of changes if we need to.
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 a FluentValidation support for Blazor by Chris Sainty. FluentValidation is a 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 the 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 the full power of FluentValidator on our forms. Check out FluentValidationValidator source code from Github repository chrissainty/FluentValidationWithRazorComponents.
Wrapping Up
There are two ways to go with validation on Blazor. We can use a 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 into internal functionality can see how Christ Sainty integrated FluentValidation to Blazor. Although there can be changes to form validation over the coming releases of Blazor, we can still start building our own advanced components to use with Blazor applications.
Published at DZone with permission of Gunnar Peipman, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments