فهرست منبع

New and improved custom fluent validation

Lukas Angerer 1 سال پیش
والد
کامیت
7f727b08a2
4فایلهای تغییر یافته به همراه168 افزوده شده و 1 حذف شده
  1. 5 1
      Components/Pages/Counter.razor
  2. 3 0
      Components/Pages/Counter.razor.cs
  3. 117 0
      CustomFluentValidationManager.cs
  4. 43 0
      CustomFluentValidator.cs

+ 5 - 1
Components/Pages/Counter.razor

@@ -16,13 +16,17 @@
     <div>
         <p>@_formMessage</p>
     </div>
-    <DataAnnotationsValidator />
+    @* <FluentValidationFormComponent Validator="_validator" /> *@
+    <CustomFluentValidator />
     <ValidationSummary />
     <div>
         <label>
             Identifier:
             <InputText @bind-Value="_model.User.Code"/>
         </label>
+        <div>
+            -- <ValidationMessage For="@(() => _model!.User.Code)" />
+        </div>
     </div>
     <div>
         <button type="submit">Submit</button>

+ 3 - 0
Components/Pages/Counter.razor.cs

@@ -1,5 +1,7 @@
 using BlazorEditForms.Model;
 
+using FluentValidation;
+
 namespace BlazorEditForms.Components.Pages;
 
 public partial class Counter
@@ -7,6 +9,7 @@ public partial class Counter
     private int currentCount = 0;
     private string _formMessage = string.Empty;
     private Aggregate? _model;
+    private IValidator _validator = new AggregateValidator();
 
     public Counter()
     {

+ 117 - 0
CustomFluentValidationManager.cs

@@ -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;
+    }
+}

+ 43 - 0
CustomFluentValidator.cs

@@ -0,0 +1,43 @@
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Forms;
+
+namespace BlazorEditForms;
+
+public class CustomFluentValidator : ComponentBase, IDisposable
+{
+    private IDisposable? _subscriptions;
+    private EditContext? _originalEditContext;
+
+    [CascadingParameter] EditContext? CurrentEditContext { get; set; }
+    [Inject] private IServiceProvider ServiceProvider { get; set; } = default!;
+
+    protected override void OnInitialized()
+    {
+        if (CurrentEditContext == null)
+        {
+            throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading " +
+                                                $"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)} " +
+                                                $"inside an EditForm.");
+        }
+
+        _subscriptions = new CustomFluentValidationManager(CurrentEditContext, ServiceProvider);
+        _originalEditContext = CurrentEditContext;
+    }
+
+    protected override void OnParametersSet()
+    {
+        if (CurrentEditContext != _originalEditContext)
+        {
+            // While we could support this, there's no known use case presently. Since InputBase doesn't support it,
+            // it's more understandable to have the same restriction.
+            throw new InvalidOperationException($"{GetType()} does not support changing the " +
+                                                $"{nameof(EditContext)} dynamically.");
+        }
+    }
+
+    public void Dispose()
+    {
+        _subscriptions?.Dispose();
+        _subscriptions = null;
+    }
+}