|
@@ -0,0 +1,117 @@
|
|
|
|
|
+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<ValidationRequestedEventArgs> _validationRequestedHandler;
|
|
|
|
|
+ private readonly EventHandler<FieldChangedEventArgs> _fieldChangedHandler;
|
|
|
|
|
+
|
|
|
|
|
+ private readonly EditContext _editContext;
|
|
|
|
|
+ private readonly IServiceProvider _serviceProvider;
|
|
|
|
|
+ private readonly IValidator<Aggregate> _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<object>.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<object>.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;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|