1

I am using Blazor Server, Fluent Validation and Blazored.FluentValidation. I have not really used Fluent Validation for front end for a very long time but I remember when I was using Jquery Validate + Fluent Validation once you tabbed out of say a textbox the basic validations would fire.

So if the field was required, and you left that textbox it would say right away it was required. If you tried to use a more complex control then the validation may not fire until you did a submit.

I can't even get the basic validation to fire immediately and wondering how it can be done.

Even if I use the sample code that comes with Blazored.FluentValidation

 public class Person
 {
     public string? FirstName { get; set; }
     public string? LastName { get; set; }
     public int? Age { get; set; }
     public string? EmailAddress { get; set; }
     public Address Address { get; set; } = new();
 }

 public class PersonValidator : AbstractValidator<Person>
 {
     public PersonValidator()
     {
         RuleSet("Names", () =>
         {
             RuleFor(p => p.FirstName)
             .NotEmpty().WithMessage("You must enter your first name")
             .MaximumLength(50).WithMessage("First name cannot be longer than 50 characters");

             RuleFor(p => p.LastName)
             .NotEmpty().WithMessage("You must enter your last name")
             .MaximumLength(50).WithMessage("Last name cannot be longer than 50 characters");
         });

         RuleFor(p => p.Age)
             .NotNull().WithMessage("You must enter your age")
             .GreaterThanOrEqualTo(0).WithMessage("Age must be greater than 0")
             .LessThan(150).WithMessage("Age cannot be greater than 150");

         RuleFor(p => p.EmailAddress)
             .NotEmpty().WithMessage("You must enter an email address")
             .EmailAddress().WithMessage("You must provide a valid email address")
             .MustAsync(async (email, _) => await IsUniqueAsync(email)).WithMessage("Email address must be unique");

         RuleFor(p => p.Address).SetValidator(new AddressValidator());
     }

     private static async Task<bool> IsUniqueAsync(string? email)
     {
         await Task.Delay(300);
         return email?.ToLower() != "[email protected]";
     }
 }

    <EditForm Model="@_person" OnValidSubmit="@SubmitValidForm">
        <FluentValidationValidator @ref="_fluentValidationValidator" DisableAssemblyScanning="@true" />
        <ValidationSummary />
    
        <p>
            <label>First Name: </label>
            <InputText @bind-Value="@_person.FirstName" />
            <ValidationMessage For="@(() => _person.FirstName)" />
        </p>
    
        <p>
            <label>Last Name: </label>
            <InputText @bind-Value="@_person.LastName" />
            <ValidationMessage For="@(() => _person.LastName)" />
        </p>
    
        <hr />
    
        <p>
            <label>Age: </label>
            <InputNumber @bind-Value="@_person.Age" />
            <ValidationMessage For="@(() => _person.Age)" />
        </p>
    
        <p>
            <label>Email Address: </label>
            <InputText @bind-Value="@_person.EmailAddress" />
            <ValidationMessage For="@(() => _person.EmailAddress)" />
        </p>
    
        <p>
            <label>Address: Line 1: </label>
            <InputText @bind-Value="@_person.Address.Line1" />
            <ValidationMessage For="@(() => _person.Address.Line1)" />
        </p>
    
        <p>
            <label>Address: Line 2: </label>
            <InputText @bind-Value="@_person.Address.Line2" />
        </p>
    
        <p>
            <label>Address: Town: </label>
            <InputText @bind-Value="@_person.Address.Town" />
            <ValidationMessage For="@(() => _person.Address.Town)" />
        </p>
    
        <p>
            <label>Address: County: </label>
            <InputText @bind-Value="@_person.Address.County" />
            <ValidationMessage For="@(() => _person.Address.County)" />
        </p>
    
        <p>
            <label>Address: Postcode: </label>
            <InputText @bind-Value="@_person.Address.Postcode" />
            <ValidationMessage For="@(() => _person.Address.Postcode)" />
        </p>
    
        <button type="submit">Save</button>
    
    </EditForm>
    <br />
    <button @onclick="PartialValidate">Partial Validation</button>
    
    @code {
        private readonly Person _person = new();
        private FluentValidationValidator? _fluentValidationValidator;
    
        private void SubmitValidForm() 
            => Console.WriteLine("Form Submitted Successfully!");
    
        private void PartialValidate() 
            => Console.WriteLine($"Partial validation result : {_fluentValidationValidator?.Validate(options => options.IncludeRuleSets("Names"))}");
    }

I only see the validation if I use the submit buttons or if I go into say the "age" box and type an age that is not valid (199) then I will see the validation but if I just go into the box and tab out nothing happens.

I want to be able to go into the empty box and then tab out and see a "required message"

Is this still possible?

1
  • you could try this: <EditForm EditContext="_editContext" OnValidSubmit="@SubmitValidForm"> <FluentValidationValidator @ref="_fluentValidationValidator" /> <ValidationSummary /> <p> <label>First Name: </label> <InputText @bind-Value="_person.FirstName" @onblur="() => ValidateField(nameof(_person.FirstName))" /> <ValidationMessage For="@(() => _person.FirstName)" /> </p> Commented Jul 2, 2024 at 10:30

1 Answer 1

1

The problem is that if no changes occur on a blank required field, there's no bind event and therefore no OnValidationRequested event raised for registered validators to respond to.

You can use the OnFocusOut event with some some logic to detect if a bind event has occurred.

Here's demo code based on the code example from the Blazored.FluentNavigation repository. I've added some commentary to explain what's happening.

@page "/"

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

<EditForm EditContext="_editContext" OnValidSubmit="@SubmitValidForm">
    <FluentValidationValidator @ref="_validator" />
    <ValidationSummary />

    <p>
        <label>First Name: </label>
        <InputText @bind-Value="@_person.FirstName" @bind-Value:after="@(() => OnAfterBindEvent("FirstName"))" @onfocusout="@((e) => OnFocusLost(e, "FirstName"))" />
    </p>
    <p>
        <label>Last Name: </label>
        <InputText @bind-Value="@_person.LastName" @bind-Value:after="@(() => OnAfterBindEvent("LastName"))" @onfocusout="@((e) => OnFocusLost(e, "LastName"))" />
    </p>

    <button type="submit">Save</button>
</EditForm>

@code {
    private FluentValidationValidator? _validator;
    private Person _person = new();
    private EditContext? _editContext;
    private readonly Dictionary<string, bool> _changeState = new();
    protected override void OnInitialized()
    {
        _editContext = new(_person);
    }

    private void SubmitValidForm()
        => Console.WriteLine("Form Submitted Successfully!");

    private void OnFocusLost(FocusEventArgs e, string fieldName)
    {
        // Only validate if there's no Bind event.
        // We raise the validation by notifying the EditContext.
        // It will raise a OnValidationRequested event which any registered validators will respond to.
        bool change = false;
        if (_changeState.TryGetValue(fieldName, out change ))
            _editContext?.NotifyFieldChanged(new(_person, fieldName));

        SetChangeValue(fieldName, false);
    }

    // This event occurs before the OnFocusOut if there's a bind action.
    // We set a flag to stop a double validation.
    private void OnAfterBindEvent(string fieldName)
    {
        SetChangeValue(fieldName, true);
    }

    private void SetChangeValue(string fieldName, bool value)
    {
        if (_changeState.ContainsKey(fieldName))
            _changeState[fieldName] = value;
        else
            _changeState.Add(fieldName, value);
    }

    private bool GetChangeValue(string fieldName)
    {
        bool change = false;
        _changeState.TryGetValue(fieldName, out change);
        return change;
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

Yeah I come up with something similar but did it onBlur and used NotifyFieldChanged. I am not sure if the "changed" flag is needed or scalable (I have like 30 fields on my screen that all need to have this behavior)
I've updated the answer so the changed flag is in a dictionary. You may not need it as you should be able to just use one bool for all the inputs: belt and braces. onBlur or onfocusout I don't think matters. You are basically trying to detect the input had been "touched" without being changed.
Cool, I will check this all out. I am having trouble clearing validation messages with this way of doing it. When using editForm I just made a button and would do _person = new() and all messages would be cleared but with this EditContext that only semi works. It will clear all values but leave all the validation border coloring around the textboxes. I have to new up a new EditContext to clear it what is apparantly not good practice.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.