1:  using Microsoft.AspNetCore.Components;
   2:  using FluentValidation;
   3:  using FluentValidation.Results;
   4:  using Microsoft.AspNetCore.Components.Forms;
   5:  using System;
   6:  using System.Threading.Tasks;
   7:   
   8:  namespace CustomValidation.Components
   9:  {
  10:      public class FluentValidationValidator : ComponentBase
  11:      {
  12:          /// <summary>
  13:          /// The EditContext cascaded to us from the EditForm component.
  14:          /// This changes whenever EditForm.Model changes
  15:          /// </summary>
  16:          [CascadingParameter]
  17:          private EditContext EditContext { get; set; }
  18:   
  19:          [Parameter]
  20:          public Type ValidatorType { get; set; }
  21:   
  22:          // Holds an instance to perform our actual validation
  23:          private IValidator Validator;
  24:   
  25:          // This is where we register our validation errors for Blazor to pick up
  26:          // in the UI. Like EditContext, this instance should be discarded when
  27:          // EditForm.Model changes (for us, that's when EditContext changes).
  28:          private ValidationMessageStore ValidationMessageStore;
  29:   
  30:          // Inject the service provider so we can create our IValidator instances
  31:          [Inject]
  32:          private IServiceProvider ServiceProvider { get; set; }
  33:   
  34:          /// <summary>
  35:          /// Executed when a parameter or cascading parameter changes. We need
  36:          /// to know if the EditContext has changed as a consequence of
  37:          /// EditForm.Model changing.
  38:          /// </summary>
  39:          public override async Task SetParametersAsync(ParameterView parameters)
  40:          {
  41:              // Keep a reference to the original values so we can check if they have changed
  42:              EditContext previousEditContext = EditContext;
  43:              Type previousValidatorType = ValidatorType;
  44:   
  45:              await base.SetParametersAsync(parameters);
  46:   
  47:              if (EditContext == null)
  48:                  throw new NullReferenceException($"{nameof(FluentValidationValidator)} must be placed within an {nameof(EditForm)}");
  49:   
  50:              if (ValidatorType == null)
  51:                  throw new NullReferenceException($"{nameof(ValidatorType)} must be specified.");
  52:   
  53:              if (!typeof(IValidator).IsAssignableFrom(ValidatorType))
  54:                  throw new ArgumentException($"{ValidatorType.Name} must implement {typeof(IValidator).FullName}");
  55:   
  56:              if (ValidatorType != previousValidatorType)
  57:                  ValidatorTypeChanged();
  58:   
  59:              // If the EditForm.Model changes then we get a new EditContext
  60:              // and need to hook it up
  61:              if (EditContext != previousEditContext)
  62:                  EditContextChanged();
  63:          }
  64:   
  65:          /// <summary>
  66:          /// We create a new instance of the validator whenever ValidatorType changes.
  67:          /// </summary>
  68:          private void ValidatorTypeChanged()
  69:          {
  70:              Validator = (IValidator)ServiceProvider.GetService(ValidatorType);
  71:          }
  72:   
  73:          /// <summary>
  74:          /// We trigger this when SetParametersAsync is executed and results in us having a
  75:          /// new EditContext.
  76:          /// </summary>
  77:          void EditContextChanged()
  78:          {
  79:              System.Diagnostics.Debug.WriteLine("EditContext has changed");
  80:   
  81:              // We need this to store our validation errors
  82:              // Whenever we get a new EditContext (because EditForm.Model has changed)
  83:              // we also need to discard our old message store and create a new one
  84:              ValidationMessageStore = new ValidationMessageStore(EditContext);
  85:              System.Diagnostics.Debug.WriteLine("New ValidationMessageStore created");
  86:   
  87:              // Observe any changes to the EditForm.Model object
  88:              HookUpEditContextEvents();
  89:          }
  90:   
  91:          private void HookUpEditContextEvents()
  92:          {
  93:              // We need to know when to validate the whole object, this
  94:              // is triggered when the EditForm is submitted
  95:              EditContext.OnValidationRequested += ValidationRequested;
  96:   
  97:              // We need to know when to validate an individual property, this
  98:              // is triggered when the user edits something
  99:              EditContext.OnFieldChanged += FieldChanged;
 100:   
 101:              System.Diagnostics.Debug.WriteLine("Hooked up EditContext events (OnValidationRequested and OnFieldChanged)");
 102:          }
 103:   
 104:          async void ValidationRequested(object sender, ValidationRequestedEventArgs args)
 105:          {
 106:              System.Diagnostics.Debug.WriteLine("OnValidationRequested triggered: Validating whole object");
 107:   
 108:              // Clear all errors from a previous validation
 109:              ValidationMessageStore.Clear();
 110:   
 111:              // Tell FluentValidation to validate the object
 112:              ValidationResult result = await Validator.ValidateAsync(EditContext.Model);
 113:   
 114:              // Now add the results to the ValidationMessageStore we created
 115:              AddValidationResult(EditContext.Model, result);
 116:          }
 117:   
 118:          async void FieldChanged(object sender, FieldChangedEventArgs args)
 119:          {
 120:              System.Diagnostics.Debug.WriteLine($"OnFieldChanged triggered: Validating a single property named {args.FieldIdentifier.FieldName}" +
 121:                  $" on class {args.FieldIdentifier.Model.GetType().Name}");
 122:   
 123:              // Create a FieldIdentifier to identify which property
 124:              // of an an object has been modified
 125:              FieldIdentifier fieldIdentifier = args.FieldIdentifier;
 126:   
 127:              // Make sure we clear out errors from a previous validation
 128:              // only for this Object+Property
 129:              ValidationMessageStore.Clear(fieldIdentifier);
 130:   
 131:              // FluentValidation specific, we need to tell it to only validate
 132:              // a specific property
 133:              var propertiesToValidate = new string[] { fieldIdentifier.FieldName };
 134:              var fluentValidationContext =
 135:                  new ValidationContext(
 136:                      instanceToValidate: fieldIdentifier.Model,
 137:                      propertyChain: new FluentValidation.Internal.PropertyChain(),
 138:                      validatorSelector: new FluentValidation.Internal.MemberNameValidatorSelector(propertiesToValidate)
 139:                  );
 140:   
 141:              // Tell FluentValidation to validate the specified property on the object that was edited
 142:              ValidationResult result = await Validator.ValidateAsync(fluentValidationContext);
 143:   
 144:              // Now add the results to the ValidationMessageStore we created
 145:              AddValidationResult(fieldIdentifier.Model, result);
 146:          }
 147:   
 148:          /// <summary>
 149:          /// Adds all of the errors from the Fluent Validator to the ValidationMessageStore
 150:          /// we created when the EditContext changed
 151:          /// </summary>
 152:          void AddValidationResult(object model, ValidationResult validationResult)
 153:          {
 154:              foreach (ValidationFailure error in validationResult.Errors)
 155:              {
 156:                  var fieldIdentifier = new FieldIdentifier(model, error.PropertyName);
 157:                  ValidationMessageStore.Add(fieldIdentifier, error.ErrorMessage);
 158:              }
 159:              EditContext.NotifyValidationStateChanged();
 160:          }
 161:      }
 162:  }