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: }