-
Notifications
You must be signed in to change notification settings - Fork 9.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Ensure validation result that are not associated with a member are recorded. Fixes #10643 * Add support for showing model-specific errors to ValidationSummary * Add support for nested validation and a more suitable CompareAttribute. Fixes #10526
- Loading branch information
Showing
23 changed files
with
1,099 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
src/Components/Blazor/Validation/src/BlazorCompareAttribute.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
namespace System.ComponentModel.DataAnnotations | ||
{ | ||
/// <summary> | ||
/// A <see cref="ValidationAttribute"/> that compares two properties | ||
/// </summary> | ||
public sealed class BlazorCompareAttribute : CompareAttribute | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of <see cref="BlazorCompareAttribute"/>. | ||
/// </summary> | ||
/// <param name="otherProperty">The property to compare with the current property.</param> | ||
public BlazorCompareAttribute(string otherProperty) | ||
: base(otherProperty) | ||
{ | ||
} | ||
|
||
/// <inheritdoc /> | ||
protected override ValidationResult IsValid(object value, ValidationContext validationContext) | ||
{ | ||
var validationResult = base.IsValid(value, validationContext); | ||
if (validationResult == ValidationResult.Success) | ||
{ | ||
return validationResult; | ||
} | ||
|
||
return new ValidationResult(validationResult.ErrorMessage, new[] { validationContext.MemberName }); | ||
} | ||
} | ||
} | ||
|
116 changes: 116 additions & 0 deletions
116
src/Components/Blazor/Validation/src/BlazorDataAnnotationsValidator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
using System.ComponentModel.DataAnnotations; | ||
using System.Linq; | ||
|
||
namespace Microsoft.AspNetCore.Components.Forms | ||
{ | ||
public class BlazorDataAnnotationsValidator : ComponentBase | ||
{ | ||
private static readonly object ValidationContextValidatorKey = new object(); | ||
private ValidationMessageStore _validationMessageStore; | ||
|
||
[CascadingParameter] | ||
internal EditContext EditContext { get; set; } | ||
|
||
protected override void OnInitialized() | ||
{ | ||
_validationMessageStore = new ValidationMessageStore(EditContext); | ||
|
||
// Perform object-level validation (starting from the root model) on request | ||
EditContext.OnValidationRequested += (sender, eventArgs) => | ||
{ | ||
_validationMessageStore.Clear(); | ||
ValidateObject(EditContext.Model); | ||
EditContext.NotifyValidationStateChanged(); | ||
}; | ||
|
||
// Perform per-field validation on each field edit | ||
EditContext.OnFieldChanged += (sender, eventArgs) => | ||
ValidateField(EditContext, _validationMessageStore, eventArgs.FieldIdentifier); | ||
} | ||
|
||
internal void ValidateObject(object value) | ||
{ | ||
if (value is null) | ||
{ | ||
return; | ||
} | ||
|
||
if (value is IEnumerable<object> enumerable) | ||
{ | ||
var index = 0; | ||
foreach (var item in enumerable) | ||
{ | ||
ValidateObject(item); | ||
index++; | ||
} | ||
|
||
return; | ||
} | ||
|
||
var validationResults = new List<ValidationResult>(); | ||
ValidateObject(value, validationResults); | ||
|
||
// Transfer results to the ValidationMessageStore | ||
foreach (var validationResult in validationResults) | ||
{ | ||
if (!validationResult.MemberNames.Any()) | ||
{ | ||
_validationMessageStore.Add(new FieldIdentifier(value, string.Empty), validationResult.ErrorMessage); | ||
continue; | ||
} | ||
|
||
foreach (var memberName in validationResult.MemberNames) | ||
{ | ||
var fieldIdentifier = new FieldIdentifier(value, memberName); | ||
_validationMessageStore.Add(fieldIdentifier, validationResult.ErrorMessage); | ||
} | ||
} | ||
} | ||
|
||
private void ValidateObject(object value, List<ValidationResult> validationResults) | ||
{ | ||
var validationContext = new ValidationContext(value); | ||
validationContext.Items.Add(ValidationContextValidatorKey, this); | ||
Validator.TryValidateObject(value, validationContext, validationResults, validateAllProperties: true); | ||
} | ||
|
||
internal static bool TryValidateRecursive(object value, ValidationContext validationContext) | ||
{ | ||
if (validationContext.Items.TryGetValue(ValidationContextValidatorKey, out var result) && result is BlazorDataAnnotationsValidator validator) | ||
{ | ||
validator.ValidateObject(value); | ||
|
||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private static void ValidateField(EditContext editContext, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier) | ||
{ | ||
// DataAnnotations only validates public properties, so that's all we'll look for | ||
var propertyInfo = fieldIdentifier.Model.GetType().GetProperty(fieldIdentifier.FieldName); | ||
if (propertyInfo != null) | ||
{ | ||
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(); | ||
} | ||
} | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
...nents/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>netstandard2.0</TargetFramework> | ||
<Description>Provides experimental support for validation of complex properties using DataAnnotations.</Description> | ||
<IsShippingPackage>true</IsShippingPackage> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Reference Include="Microsoft.AspNetCore.Components.Forms" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<InternalsVisibleTo Include="Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests" /> | ||
</ItemGroup> | ||
|
||
</Project> |
29 changes: 29 additions & 0 deletions
29
src/Components/Blazor/Validation/src/ValidateComplexTypeAttribute.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using Microsoft.AspNetCore.Components.Forms; | ||
|
||
namespace System.ComponentModel.DataAnnotations | ||
{ | ||
/// <summary> | ||
/// A <see cref="ValidationAttribute"/> that indicates that the property is a complex or collection type that further needs to be validated. | ||
/// <para> | ||
/// By default <see cref="Validator"/> does not recurse in to complex property types during validation. When used in conjunction with <see cref="BlazorDataAnnotationsValidator"/>, | ||
/// this property allows the validation system to validate complex or collection type properties. | ||
/// </para> | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] | ||
public sealed class ValidateComplexTypeAttribute : ValidationAttribute | ||
{ | ||
/// <inheritdoc /> | ||
protected override ValidationResult IsValid(object value, ValidationContext validationContext) | ||
{ | ||
if (!BlazorDataAnnotationsValidator.TryValidateRecursive(value, validationContext)) | ||
{ | ||
throw new InvalidOperationException($"{nameof(ValidateComplexTypeAttribute)} can only used with {nameof(BlazorDataAnnotationsValidator)}."); | ||
} | ||
|
||
return ValidationResult.Success; | ||
} | ||
} | ||
} |
Oops, something went wrong.