-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Ability to deactivate DataAnnotationsValidation dynamically. Fixes #31027 #31413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
17a6e5b
d91dd28
01c7eed
ea62a4b
52f11c8
0b891f1
304063c
1dfc4a5
8719095
fe0d674
c987b90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,98 +16,118 @@ namespace Microsoft.AspNetCore.Components.Forms | |
/// </summary> | ||
public static class EditContextDataAnnotationsExtensions | ||
{ | ||
private static ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache | ||
= new ConcurrentDictionary<(Type, string), PropertyInfo?>(); | ||
|
||
/// <summary> | ||
/// Adds DataAnnotations validation support to the <see cref="EditContext"/>. | ||
/// </summary> | ||
/// <param name="editContext">The <see cref="EditContext"/>.</param> | ||
[Obsolete("Use " + nameof(EnableDataAnnotationsValidation) + " instead.")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should do a breaking change announcement for this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, will do so if/when this is merged. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
public static EditContext AddDataAnnotationsValidation(this EditContext editContext) | ||
{ | ||
if (editContext == null) | ||
{ | ||
throw new ArgumentNullException(nameof(editContext)); | ||
} | ||
|
||
var messages = new ValidationMessageStore(editContext); | ||
|
||
// Perform object-level validation on request | ||
editContext.OnValidationRequested += | ||
(sender, eventArgs) => ValidateModel((EditContext)sender!, messages); | ||
|
||
// Perform per-field validation on each field edit | ||
editContext.OnFieldChanged += | ||
(sender, eventArgs) => ValidateField(editContext, messages, eventArgs.FieldIdentifier); | ||
|
||
EnableDataAnnotationsValidation(editContext); | ||
return editContext; | ||
} | ||
|
||
private static void ValidateModel(EditContext editContext, ValidationMessageStore messages) | ||
/// <summary> | ||
/// Enables DataAnnotations validation support for the <see cref="EditContext"/>. | ||
/// </summary> | ||
/// <param name="editContext">The <see cref="EditContext"/>.</param> | ||
/// <returns>A disposable object whose disposal will remove DataAnnotations validation support from the <see cref="EditContext"/>.</returns> | ||
public static IDisposable EnableDataAnnotationsValidation(this EditContext editContext) | ||
{ | ||
return new DataAnnotationsEventSubscriptions(editContext); | ||
} | ||
|
||
private sealed class DataAnnotationsEventSubscriptions : IDisposable | ||
{ | ||
var validationContext = new ValidationContext(editContext.Model); | ||
var validationResults = new List<ValidationResult>(); | ||
Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true); | ||
private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache = new(); | ||
|
||
// Transfer results to the ValidationMessageStore | ||
messages.Clear(); | ||
foreach (var validationResult in validationResults) | ||
private readonly EditContext _editContext; | ||
private readonly ValidationMessageStore _messages; | ||
|
||
public DataAnnotationsEventSubscriptions(EditContext editContext) | ||
{ | ||
if (validationResult == null) | ||
{ | ||
continue; | ||
} | ||
_editContext = editContext ?? throw new ArgumentNullException(nameof(editContext)); | ||
_messages = new ValidationMessageStore(_editContext); | ||
|
||
if (!validationResult.MemberNames.Any()) | ||
{ | ||
messages.Add(new FieldIdentifier(editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage!); | ||
continue; | ||
} | ||
_editContext.OnFieldChanged += OnFieldChanged; | ||
_editContext.OnValidationRequested += OnValidationRequested; | ||
} | ||
|
||
foreach (var memberName in validationResult.MemberNames) | ||
private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs) | ||
{ | ||
var fieldIdentifier = eventArgs.FieldIdentifier; | ||
if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo)) | ||
{ | ||
messages.Add(editContext.Field(memberName), validationResult.ErrorMessage!); | ||
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model); | ||
var validationContext = new ValidationContext(fieldIdentifier.Model) | ||
{ | ||
MemberName = propertyInfo.Name | ||
}; | ||
var results = new List<ValidationResult>(); | ||
|
||
Validator.TryValidateProperty(propertyValue, validationContext, results); | ||
_messages.Clear(fieldIdentifier); | ||
_messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage!)); | ||
|
||
// We have to notify even if there were no messages before and are still no messages now, | ||
// because the "state" that changed might be the completion of some async validation task | ||
_editContext.NotifyValidationStateChanged(); | ||
} | ||
} | ||
|
||
editContext.NotifyValidationStateChanged(); | ||
} | ||
|
||
private static void ValidateField(EditContext editContext, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier) | ||
{ | ||
if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo)) | ||
private void OnValidationRequested(object? sender, ValidationRequestedEventArgs e) | ||
{ | ||
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model); | ||
var validationContext = new ValidationContext(fieldIdentifier.Model) | ||
var validationContext = new ValidationContext(_editContext.Model); | ||
var validationResults = new List<ValidationResult>(); | ||
Validator.TryValidateObject(_editContext.Model, validationContext, validationResults, true); | ||
|
||
// Transfer results to the ValidationMessageStore | ||
_messages.Clear(); | ||
foreach (var validationResult in validationResults) | ||
{ | ||
MemberName = propertyInfo.Name | ||
}; | ||
var results = new List<ValidationResult>(); | ||
if (validationResult == null) | ||
{ | ||
continue; | ||
} | ||
|
||
if (!validationResult.MemberNames.Any()) | ||
{ | ||
_messages.Add(new FieldIdentifier(_editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage!); | ||
continue; | ||
} | ||
|
||
foreach (var memberName in validationResult.MemberNames) | ||
{ | ||
_messages.Add(_editContext.Field(memberName), validationResult.ErrorMessage!); | ||
} | ||
} | ||
|
||
Validator.TryValidateProperty(propertyValue, validationContext, results); | ||
messages.Clear(fieldIdentifier); | ||
messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage!)); | ||
_editContext.NotifyValidationStateChanged(); | ||
} | ||
|
||
// We have to notify even if there were no messages before and are still no messages now, | ||
// because the "state" that changed might be the completion of some async validation task | ||
editContext.NotifyValidationStateChanged(); | ||
public void Dispose() | ||
{ | ||
_messages.Clear(); | ||
_editContext.OnFieldChanged -= OnFieldChanged; | ||
_editContext.OnValidationRequested -= OnValidationRequested; | ||
_editContext.NotifyValidationStateChanged(); | ||
} | ||
} | ||
|
||
private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo? propertyInfo) | ||
{ | ||
var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName); | ||
if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo)) | ||
private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo? propertyInfo) | ||
{ | ||
// DataAnnotations only validates public properties, so that's all we'll look for | ||
// If we can't find it, cache 'null' so we don't have to try again next time | ||
propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName); | ||
var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName); | ||
if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo)) | ||
{ | ||
// DataAnnotations only validates public properties, so that's all we'll look for | ||
// If we can't find it, cache 'null' so we don't have to try again next time | ||
propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName); | ||
|
||
// No need to lock, because it doesn't matter if we write the same value twice | ||
_propertyInfoCache[cacheKey] = propertyInfo; | ||
} | ||
// No need to lock, because it doesn't matter if we write the same value twice | ||
_propertyInfoCache[cacheKey] = propertyInfo; | ||
} | ||
|
||
return propertyInfo != null; | ||
return propertyInfo != null; | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
#nullable enable | ||
override Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator.OnParametersSet() -> void | ||
static Microsoft.AspNetCore.Components.Forms.EditContextDataAnnotationsExtensions.EnableDataAnnotationsValidation(this Microsoft.AspNetCore.Components.Forms.EditContext! editContext) -> System.IDisposable! | ||
virtual Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator.Dispose(bool disposing) -> void |
Uh oh!
There was an error while loading. Please reload this page.