using BlazorEditForms.Model; using FluentValidation; using FluentValidation.Internal; using FluentValidation.Validators; using Microsoft.AspNetCore.Components.Forms; namespace BlazorEditForms; public class CustomFluentValidationManager : IDisposable { private readonly EventHandler _validationRequestedHandler; private readonly EventHandler _fieldChangedHandler; private readonly EditContext _editContext; private readonly IServiceProvider _serviceProvider; private readonly IValidator _validator; private readonly ValidationMessageStore _validationMessageStore; public CustomFluentValidationManager(EditContext editContext, IServiceProvider serviceProvider) { _editContext = editContext; _serviceProvider = serviceProvider; _validator = new AggregateValidator(); _validationRequestedHandler = async (sender, eventArgs) => await ValidateModel(); _editContext.OnValidationRequested += _validationRequestedHandler; _fieldChangedHandler = async (sender, eventArgs) => await ValidateField(eventArgs.FieldIdentifier); _editContext.OnFieldChanged += _fieldChangedHandler; // This validation message store is _internally_ connected to the edit context and essentially serves as a // separate API to manage the validation messages that can (with public methods) only be _extracted from_ the // edit context. _validationMessageStore = new ValidationMessageStore(_editContext); // TODO: how to make this multi-threaded ? ValidatorOptions.Global.OnFailureCreated = (failure, context, value, rule, component) => { if (!string.IsNullOrEmpty(rule.PropertyName)) { failure.FormattedMessagePlaceholderValues["MINE"] = new FieldIdentifier(context.InstanceToValidate, rule.PropertyName); } return failure; }; } private async Task ValidateModel() { var validationContext = ValidationContext.CreateWithOptions(_editContext.Model, strategy => { //strategy.UseCustomSelector(new MySelector()); strategy.IncludeRulesNotInRuleSet(); }); var validationResult = await _validator.ValidateAsync(validationContext); _validationMessageStore.Clear(); foreach (var failure in validationResult.Errors) { if (failure.FormattedMessagePlaceholderValues.TryGetValue("MINE", out var mine)) { var fieldIdentifier = (FieldIdentifier)mine; _validationMessageStore.Add(fieldIdentifier, failure.ErrorMessage); } } _editContext.NotifyValidationStateChanged(); } private async Task ValidateField(FieldIdentifier fieldIdentifier) { var validationContext = ValidationContext.CreateWithOptions(_editContext.Model, strategy => { strategy.UseCustomSelector(new FieldIdentifierSelector(fieldIdentifier)); //strategy.IncludeRulesNotInRuleSet(); }); var validationResult = await _validator.ValidateAsync(validationContext); _validationMessageStore.Clear(fieldIdentifier); foreach (var failure in validationResult.Errors) { if (failure.FormattedMessagePlaceholderValues.TryGetValue("MINE", out var mine)) { if (fieldIdentifier.Equals(mine)) { _validationMessageStore.Add(fieldIdentifier, failure.ErrorMessage); } } } _editContext.NotifyValidationStateChanged(); } private class FieldIdentifierSelector : IValidatorSelector { private readonly FieldIdentifier _fieldIdentifier; public FieldIdentifierSelector(FieldIdentifier fieldIdentifier) { _fieldIdentifier = fieldIdentifier; } public bool CanExecute(IValidationRule rule, string propertyPath, IValidationContext context) { return rule.Components.Any(c => c.Validator is IChildValidatorAdaptor) || (context.InstanceToValidate == _fieldIdentifier.Model && rule.PropertyName == _fieldIdentifier.FieldName); } } public void Dispose() { _editContext.OnValidationRequested -= _validationRequestedHandler; _editContext.OnFieldChanged -= _fieldChangedHandler; } }