From 0c3f7599ed610a6766ffb2335b496176c19b4cd5 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 25 Sep 2019 14:29:22 -0700 Subject: [PATCH 1/9] Validation fixes for Blazor * Ensure validation result that are not associated with a member are recorded. Fixes https://github.com/aspnet/AspNetCore/issues/10643 * Add support for showing model-specific errors to ValidationSummary * Add support for nested validation and a more suitable CompareAttribute. Fixes https://github.com/aspnet/AspNetCore/issues/10526 --- eng/ProjectReferences.props | 1 + .../Validation/src/BlazorCompareAttribute.cs | 33 ++ .../src/BlazorDataAnnotationsValidator.cs | 116 +++++ ...e.Blazor.DataAnnotations.Validation.csproj | 17 + .../src/ValidateComplexTypeAttribute.cs | 29 ++ .../test/BlazorDatAnnotationsValidatorTest.cs | 421 ++++++++++++++++++ ...or.DataAnnotations.Validation.Tests.csproj | 11 + src/Components/Components.sln | 33 ++ src/Components/ComponentsNoDeps.slnf | 2 + .../EditContextDataAnnotationsExtensions.cs | 6 + .../BlazorServerApp/BlazorServerApp.csproj | 1 + .../Web/src/Forms/ValidationSummary.cs | 39 +- .../test/E2ETest/Tests/FormsTest.cs | 70 ++- .../FormsTestWithExperimentalValiator.cs | 90 ++++ .../BasicTestApp/BasicTestApp.csproj | 1 + .../ExperimentalValidationComponent.razor | 181 ++++++++ .../FormsTest/SimpleValidationComponent.razor | 11 +- ...tionComponentUsingExperimentalValidator.cs | 7 + .../TypicalValidationComponent.razor | 20 + ...tionComponentUsingExperimentalValidator.cs | 7 + .../test/testassets/BasicTestApp/Index.razor | 3 + .../BrowserAssertFailedException.cs | 31 +- src/Shared/E2ETesting/WaitAssert.cs | 4 +- 23 files changed, 1097 insertions(+), 37 deletions(-) create mode 100644 src/Components/Blazor/Validation/src/BlazorCompareAttribute.cs create mode 100644 src/Components/Blazor/Validation/src/BlazorDataAnnotationsValidator.cs create mode 100644 src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj create mode 100644 src/Components/Blazor/Validation/src/ValidateComplexTypeAttribute.cs create mode 100644 src/Components/Blazor/Validation/test/BlazorDatAnnotationsValidatorTest.cs create mode 100644 src/Components/Blazor/Validation/test/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj create mode 100644 src/Components/test/E2ETest/Tests/FormsTestWithExperimentalValiator.cs create mode 100644 src/Components/test/testassets/BasicTestApp/FormsTest/ExperimentalValidationComponent.razor create mode 100644 src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponentUsingExperimentalValidator.cs create mode 100644 src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponentUsingExperimentalValidator.cs diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 4210405c67dd..37704e933ee5 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -137,6 +137,7 @@ + diff --git a/src/Components/Blazor/Validation/src/BlazorCompareAttribute.cs b/src/Components/Blazor/Validation/src/BlazorCompareAttribute.cs new file mode 100644 index 000000000000..f180b1b47936 --- /dev/null +++ b/src/Components/Blazor/Validation/src/BlazorCompareAttribute.cs @@ -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 +{ + /// + /// A that compares two properties + /// + public sealed class BlazorCompareAttribute : CompareAttribute + { + /// + /// Initializes a new instance of . + /// + /// The property to compare with the current property. + public BlazorCompareAttribute(string otherProperty) + : base(otherProperty) + { + } + + /// + 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 }); + } + } +} + diff --git a/src/Components/Blazor/Validation/src/BlazorDataAnnotationsValidator.cs b/src/Components/Blazor/Validation/src/BlazorDataAnnotationsValidator.cs new file mode 100644 index 000000000000..27a8ef4a9327 --- /dev/null +++ b/src/Components/Blazor/Validation/src/BlazorDataAnnotationsValidator.cs @@ -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 enumerable) + { + var index = 0; + foreach (var item in enumerable) + { + ValidateObject(item); + index++; + } + + return; + } + + var validationResults = new List(); + 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 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(); + + 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(); + } + } + } +} diff --git a/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj b/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj new file mode 100644 index 000000000000..c7621001175d --- /dev/null +++ b/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + Provides experimental support for validation of complex properties using DataAnnotations. + true + + + + + + + + + + + diff --git a/src/Components/Blazor/Validation/src/ValidateComplexTypeAttribute.cs b/src/Components/Blazor/Validation/src/ValidateComplexTypeAttribute.cs new file mode 100644 index 000000000000..9203621d13a5 --- /dev/null +++ b/src/Components/Blazor/Validation/src/ValidateComplexTypeAttribute.cs @@ -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 +{ + /// + /// A that indicates that the property is a complex or collection type that further needs to be validated. + /// + /// By default does not recurse in to complex property types during validation. When used in conjunction with , + /// this property allows the validation system to validate complex or collection type properties. + /// + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public sealed class ValidateComplexTypeAttribute : ValidationAttribute + { + /// + 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; + } + } +} diff --git a/src/Components/Blazor/Validation/test/BlazorDatAnnotationsValidatorTest.cs b/src/Components/Blazor/Validation/test/BlazorDatAnnotationsValidatorTest.cs new file mode 100644 index 000000000000..4cea91d1161b --- /dev/null +++ b/src/Components/Blazor/Validation/test/BlazorDatAnnotationsValidatorTest.cs @@ -0,0 +1,421 @@ +// 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; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.AspNetCore.Components.Forms; +using Xunit; + +namespace Microsoft.AspNetCore.Components +{ + public class BlazorDatAnnotationsValidatorTest + { + public class SimpleModel + { + [Required] + public string Name { get; set; } + + [Range(1, 16)] + public int Age { get; set; } + } + + [Fact] + public void ValidateObject_SimpleObject() + { + var model = new SimpleModel + { + Age = 23, + }; + + var editContext = Validate(model); + var messages = editContext.GetValidationMessages(() => model.Name); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => model.Age); + Assert.Single(messages); + + Assert.Equal(2, editContext.GetValidationMessages().Count()); + } + + [Fact] + public void ValidateObject_SimpleObject_AllValid() + { + var model = new SimpleModel { Name = "Test", Age = 5 }; + + var editContext = Validate(model); + var messages = editContext.GetValidationMessages(() => model.Name); + Assert.Empty(messages); + + messages = editContext.GetValidationMessages(() => model.Age); + Assert.Empty(messages); + + Assert.Empty(editContext.GetValidationMessages()); + } + + public class ModelWithComplexProperty + { + [Required] + public string Property1 { get; set; } + + [ValidateComplexType] + public SimpleModel SimpleModel { get; set; } + } + + [Fact] + public void ValidateObject_NullComplexProperty() + { + var model = new ModelWithComplexProperty(); + + var editContext = Validate(model); + var messages = editContext.GetValidationMessages(() => model.Property1); + Assert.Single(messages); + + Assert.Single(editContext.GetValidationMessages()); + } + + [Fact] + public void ValidateObject_ModelWithComplexProperties() + { + var model = new ModelWithComplexProperty { SimpleModel = new SimpleModel() }; + + var editContext = Validate(model); + var messages = editContext.GetValidationMessages(() => model.Property1); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => model.SimpleModel); + Assert.Empty(messages); + + messages = editContext.GetValidationMessages(() => model.SimpleModel.Age); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => model.SimpleModel.Name); + Assert.Single(messages); + + Assert.Equal(3, editContext.GetValidationMessages().Count()); + } + + [Fact] + public void ValidateObject_ModelWithComplexProperties_SomeValid() + { + var model = new ModelWithComplexProperty + { + Property1 = "Value", + SimpleModel = new SimpleModel { Name = "Some Value" }, + }; + + var editContext = Validate(model); + var messages = editContext.GetValidationMessages(() => model.Property1); + Assert.Empty(messages); + + messages = editContext.GetValidationMessages(() => model.SimpleModel); + Assert.Empty(messages); + + messages = editContext.GetValidationMessages(() => model.SimpleModel.Age); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => model.SimpleModel.Name); + Assert.Empty(messages); + + Assert.Single(editContext.GetValidationMessages()); + } + + public class TestValidatableObject : IValidatableObject + { + [Required] + public string Name { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + yield return new ValidationResult("Custom validation error"); + } + } + + public class ModelWithValidatableComplexProperty + { + [Required] + public string Property1 { get; set; } + + [ValidateComplexType] + public TestValidatableObject Property2 { get; set; } = new TestValidatableObject(); + } + + [Fact] + public void ValidateObject_ValidatableComplexProperty() + { + var model = new ModelWithValidatableComplexProperty(); + + var editContext = Validate(model); + var messages = editContext.GetValidationMessages(() => model.Property1); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => model.Property2); + Assert.Empty(messages); + + messages = editContext.GetValidationMessages(() => model.Property2.Name); + Assert.Single(messages); + + Assert.Equal(2, editContext.GetValidationMessages().Count()); + } + + [Fact] + public void ValidateObject_ValidatableComplexProperty_ValidatesIValidatableProperty() + { + var model = new ModelWithValidatableComplexProperty + { + Property2 = new TestValidatableObject { Name = "test" }, + }; + + var editContext = Validate(model); + var messages = editContext.GetValidationMessages(() => model.Property1); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => model.Property2); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => model.Property2.Name); + Assert.Empty(messages); + + Assert.Equal(2, editContext.GetValidationMessages().Count()); + } + + [Fact] + public void ValidateObject_ModelIsIValidatable_PropertyHasError() + { + var model = new TestValidatableObject(); + + var editContext = Validate(model); + var messages = editContext.GetValidationMessages(new FieldIdentifier(model, string.Empty)); + Assert.Empty(messages); + + messages = editContext.GetValidationMessages(() => model.Name); + Assert.Single(messages); + + Assert.Single(editContext.GetValidationMessages()); + } + + [Fact] + public void ValidateObject_ModelIsIValidatable_ModelHasError() + { + var model = new TestValidatableObject { Name = "test" }; + + var editContext = Validate(model); + var messages = editContext.GetValidationMessages(new FieldIdentifier(model, string.Empty)); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => model.Name); + Assert.Empty(messages); + + Assert.Single(editContext.GetValidationMessages()); + } + + [Fact] + public void ValidateObject_CollectionModel() + { + var model = new List + { + new SimpleModel(), + new SimpleModel { Name = "test", }, + }; + + var editContext = Validate(model); + + var item = model[0]; + var messages = editContext.GetValidationMessages(new FieldIdentifier(model, "0")); + Assert.Empty(messages); + + messages = editContext.GetValidationMessages(() => item.Name); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => item.Age); + Assert.Single(messages); + + item = model[1]; + messages = editContext.GetValidationMessages(new FieldIdentifier(model, "1")); + Assert.Empty(messages); + + messages = editContext.GetValidationMessages(() => item.Name); + Assert.Empty(messages); + + messages = editContext.GetValidationMessages(() => item.Age); + Assert.Single(messages); + + Assert.Equal(3, editContext.GetValidationMessages().Count()); + } + + [Fact] + public void ValidateObject_CollectionValidatableModel() + { + var model = new List + { + new TestValidatableObject(), + new TestValidatableObject { Name = "test", }, + }; + + var editContext = Validate(model); + + var item = model[0]; + var messages = editContext.GetValidationMessages(() => item.Name); + Assert.Single(messages); + + item = model[1]; + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => item.Name); + Assert.Empty(messages); + + Assert.Equal(2, editContext.GetValidationMessages().Count()); + } + + private class Level1Validation + { + [ValidateComplexType] + public Level2Validation Level2 { get; set; } + } + + public class Level2Validation + { + [ValidateComplexType] + public SimpleModel Level3 { get; set; } + } + + [Fact] + public void ValidateObject_ManyLevels() + { + var model = new Level1Validation + { + Level2 = new Level2Validation + { + Level3 = new SimpleModel + { + Age = 47, + } + } + }; + + var editContext = Validate(model); + var level3 = model.Level2.Level3; + + var messages = editContext.GetValidationMessages(() => level3.Name); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => level3.Age); + Assert.Single(messages); + + Assert.Equal(2, editContext.GetValidationMessages().Count()); + } + + [Fact] + public void ValidateField_PropertyValid() + { + var model = new SimpleModel { Age = 1 }; + var fieldIdentifier = FieldIdentifier.Create(() => model.Age); + + var editContext = ValidateField(model, fieldIdentifier); + var messages = editContext.GetValidationMessages(fieldIdentifier); + Assert.Empty(messages); + + Assert.Empty(editContext.GetValidationMessages()); + } + + [Fact] + public void ValidateField_PropertyInvalid() + { + var model = new SimpleModel { Age = 42 }; + var fieldIdentifier = FieldIdentifier.Create(() => model.Age); + + var editContext = ValidateField(model, fieldIdentifier); + var messages = editContext.GetValidationMessages(fieldIdentifier); + Assert.Single(messages); + + Assert.Single(editContext.GetValidationMessages()); + } + + [Fact] + public void ValidateField_AfterSubmitValidation() + { + var model = new SimpleModel { Age = 42 }; + var fieldIdentifier = FieldIdentifier.Create(() => model.Age); + + var editContext = Validate(model); + var messages = editContext.GetValidationMessages(fieldIdentifier); + Assert.Single(messages); + + Assert.Equal(2, editContext.GetValidationMessages().Count()); + + model.Age = 4; + + editContext.NotifyFieldChanged(fieldIdentifier); + messages = editContext.GetValidationMessages(fieldIdentifier); + Assert.Empty(messages); + + Assert.Single(editContext.GetValidationMessages()); + } + + [Fact] + public void ValidateField_ModelWithComplexProperty() + { + var model = new ModelWithComplexProperty + { + SimpleModel = new SimpleModel { Age = 1 }, + }; + var fieldIdentifier = FieldIdentifier.Create(() => model.SimpleModel.Name); + + var editContext = ValidateField(model, fieldIdentifier); + var messages = editContext.GetValidationMessages(fieldIdentifier); + Assert.Single(messages); + + Assert.Single(editContext.GetValidationMessages()); + } + + [Fact] + public void ValidateField_ModelWithComplexProperty_AfterSubmitValidation() + { + var model = new ModelWithComplexProperty + { + Property1 = "test", + SimpleModel = new SimpleModel { Age = 29, Name = "Test" }, + }; + var fieldIdentifier = FieldIdentifier.Create(() => model.SimpleModel.Age); + + var editContext = Validate(model); + var messages = editContext.GetValidationMessages(fieldIdentifier); + Assert.Single(messages); + + model.SimpleModel.Age = 9; + editContext.NotifyFieldChanged(fieldIdentifier); + + messages = editContext.GetValidationMessages(fieldIdentifier); + Assert.Empty(messages); + Assert.Empty(editContext.GetValidationMessages()); + } + + private static EditContext Validate(object model) + { + var editContext = new EditContext(model); + var validator = new TestBlazorDataAnnotationsValidator { EditContext = editContext, }; + validator.OnInitialized(); + + editContext.Validate(); + + return editContext; + } + + private static EditContext ValidateField(object model, in FieldIdentifier field) + { + var editContext = new EditContext(model); + var validator = new TestBlazorDataAnnotationsValidator { EditContext = editContext, }; + validator.OnInitialized(); + + editContext.NotifyFieldChanged(field); + + return editContext; + } + + private class TestBlazorDataAnnotationsValidator : BlazorDataAnnotationsValidator + { + public new void OnInitialized() => base.OnInitialized(); + } + } +} diff --git a/src/Components/Blazor/Validation/test/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj b/src/Components/Blazor/Validation/test/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj new file mode 100644 index 000000000000..02e85615362b --- /dev/null +++ b/src/Components/Blazor/Validation/test/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj @@ -0,0 +1,11 @@ + + + + $(DefaultNetCoreTargetFramework) + + + + + + + diff --git a/src/Components/Components.sln b/src/Components/Components.sln index 046fc0b7cab5..3fcc3175b512 100644 --- a/src/Components/Components.sln +++ b/src/Components/Components.sln @@ -240,6 +240,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ignitor", "Ignitor\src\Igni EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ignitor.Test", "Ignitor\test\Ignitor.Test.csproj", "{F31E8118-014E-4CCE-8A48-5282F7B9BB3E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Validation", "Validation", "{FD9BD646-9D50-42ED-A3E1-90558BA0C6B2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.DataAnnotations.Validation", "Blazor\Validation\src\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj", "{B70F90C7-2696-4050-B24E-BF0308F4E059}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests", "Blazor\Validation\test\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj", "{A5617A9D-C71E-44DE-936C-27611EB40A02}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1486,6 +1492,30 @@ Global {F31E8118-014E-4CCE-8A48-5282F7B9BB3E}.Release|x64.Build.0 = Release|Any CPU {F31E8118-014E-4CCE-8A48-5282F7B9BB3E}.Release|x86.ActiveCfg = Release|Any CPU {F31E8118-014E-4CCE-8A48-5282F7B9BB3E}.Release|x86.Build.0 = Release|Any CPU + {B70F90C7-2696-4050-B24E-BF0308F4E059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B70F90C7-2696-4050-B24E-BF0308F4E059}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B70F90C7-2696-4050-B24E-BF0308F4E059}.Debug|x64.ActiveCfg = Debug|Any CPU + {B70F90C7-2696-4050-B24E-BF0308F4E059}.Debug|x64.Build.0 = Debug|Any CPU + {B70F90C7-2696-4050-B24E-BF0308F4E059}.Debug|x86.ActiveCfg = Debug|Any CPU + {B70F90C7-2696-4050-B24E-BF0308F4E059}.Debug|x86.Build.0 = Debug|Any CPU + {B70F90C7-2696-4050-B24E-BF0308F4E059}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B70F90C7-2696-4050-B24E-BF0308F4E059}.Release|Any CPU.Build.0 = Release|Any CPU + {B70F90C7-2696-4050-B24E-BF0308F4E059}.Release|x64.ActiveCfg = Release|Any CPU + {B70F90C7-2696-4050-B24E-BF0308F4E059}.Release|x64.Build.0 = Release|Any CPU + {B70F90C7-2696-4050-B24E-BF0308F4E059}.Release|x86.ActiveCfg = Release|Any CPU + {B70F90C7-2696-4050-B24E-BF0308F4E059}.Release|x86.Build.0 = Release|Any CPU + {A5617A9D-C71E-44DE-936C-27611EB40A02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5617A9D-C71E-44DE-936C-27611EB40A02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5617A9D-C71E-44DE-936C-27611EB40A02}.Debug|x64.ActiveCfg = Debug|Any CPU + {A5617A9D-C71E-44DE-936C-27611EB40A02}.Debug|x64.Build.0 = Debug|Any CPU + {A5617A9D-C71E-44DE-936C-27611EB40A02}.Debug|x86.ActiveCfg = Debug|Any CPU + {A5617A9D-C71E-44DE-936C-27611EB40A02}.Debug|x86.Build.0 = Debug|Any CPU + {A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|Any CPU.Build.0 = Release|Any CPU + {A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|x64.ActiveCfg = Release|Any CPU + {A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|x64.Build.0 = Release|Any CPU + {A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|x86.ActiveCfg = Release|Any CPU + {A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1596,6 +1626,9 @@ Global {BBF37AF9-8290-4B70-8BA8-0F6017B3B620} = {46E4300C-5726-4108-B9A2-18BB94EB26ED} {CD0EF85C-4187-4515-A355-E5A0D4485F40} = {BDE2397D-C53A-4783-8B3A-1F54F48A6926} {F31E8118-014E-4CCE-8A48-5282F7B9BB3E} = {BDE2397D-C53A-4783-8B3A-1F54F48A6926} + {FD9BD646-9D50-42ED-A3E1-90558BA0C6B2} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} + {B70F90C7-2696-4050-B24E-BF0308F4E059} = {FD9BD646-9D50-42ED-A3E1-90558BA0C6B2} + {A5617A9D-C71E-44DE-936C-27611EB40A02} = {FD9BD646-9D50-42ED-A3E1-90558BA0C6B2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CC3C47E1-AD1A-4619-9CD3-E08A0148E5CE} diff --git a/src/Components/ComponentsNoDeps.slnf b/src/Components/ComponentsNoDeps.slnf index ce01e6468f2e..715ea1d4fd0c 100644 --- a/src/Components/ComponentsNoDeps.slnf +++ b/src/Components/ComponentsNoDeps.slnf @@ -13,6 +13,8 @@ "Blazor\\DevServer\\src\\Microsoft.AspNetCore.Blazor.DevServer.csproj", "Blazor\\Http\\src\\Microsoft.AspNetCore.Blazor.HttpClient.csproj", "Blazor\\Http\\test\\Microsoft.AspNetCore.Blazor.HttpClient.Tests.csproj", + "Blazor\\Validation\\src\\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj", + "Blazor\\Validation\\test\\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj", "Blazor\\Server\\src\\Microsoft.AspNetCore.Blazor.Server.csproj", "Blazor\\Templates\\src\\Microsoft.AspNetCore.Blazor.Templates.csproj", "Blazor\\testassets\\HostedInAspNet.Client\\HostedInAspNet.Client.csproj", diff --git a/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs b/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs index 9fce473a415f..e7f9d5f15507 100644 --- a/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs +++ b/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs @@ -52,6 +52,12 @@ private static void ValidateModel(EditContext editContext, ValidationMessageStor messages.Clear(); foreach (var validationResult in validationResults) { + 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); diff --git a/src/Components/Samples/BlazorServerApp/BlazorServerApp.csproj b/src/Components/Samples/BlazorServerApp/BlazorServerApp.csproj index 101fe45c133b..2a82b7453aa5 100644 --- a/src/Components/Samples/BlazorServerApp/BlazorServerApp.csproj +++ b/src/Components/Samples/BlazorServerApp/BlazorServerApp.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Components/Web/src/Forms/ValidationSummary.cs b/src/Components/Web/src/Forms/ValidationSummary.cs index 8e151b1e631b..270f7871766f 100644 --- a/src/Components/Web/src/Forms/ValidationSummary.cs +++ b/src/Components/Web/src/Forms/ValidationSummary.cs @@ -19,6 +19,12 @@ public class ValidationSummary : ComponentBase, IDisposable private EditContext _previousEditContext; private readonly EventHandler _validationStateChangedHandler; + /// + /// Gets or sets the model to produce the list of validation messages for. + /// When specified, this lists all errors that are associated with the model instance. + /// + [Parameter] public object Model { get; set; } + /// /// Gets or sets a collection of additional attributes that will be applied to the created ul element. /// @@ -57,22 +63,31 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) { // As an optimization, only evaluate the messages enumerable once, and // only produce the enclosing
    if there's at least one message - var messagesEnumerator = CurrentEditContext.GetValidationMessages().GetEnumerator(); - if (messagesEnumerator.MoveNext()) - { - builder.OpenElement(0, "ul"); - builder.AddMultipleAttributes(1, AdditionalAttributes); - builder.AddAttribute(2, "class", "validation-errors"); + var validationMessages = Model is null ? + CurrentEditContext.GetValidationMessages() : + CurrentEditContext.GetValidationMessages(new FieldIdentifier(Model, string.Empty)); - do + var first = true; + foreach (var error in validationMessages) + { + if (first) { - builder.OpenElement(3, "li"); - builder.AddAttribute(4, "class", "validation-message"); - builder.AddContent(5, messagesEnumerator.Current); - builder.CloseElement(); + first = false; + + builder.OpenElement(0, "ul"); + builder.AddMultipleAttributes(1, AdditionalAttributes); + builder.AddAttribute(2, "class", "validation-errors"); } - while (messagesEnumerator.MoveNext()); + builder.OpenElement(3, "li"); + builder.AddAttribute(4, "class", "validation-message"); + builder.AddContent(5, error); + builder.CloseElement(); + } + + if (!first) + { + // We have at least one validation message. builder.CloseElement(); } } diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs index 2d622ba8642d..8ac266a4ece2 100644 --- a/src/Components/test/E2ETest/Tests/FormsTest.cs +++ b/src/Components/test/E2ETest/Tests/FormsTest.cs @@ -1,18 +1,17 @@ // 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; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; using BasicTestApp; using BasicTestApp.FormsTest; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; -using Microsoft.AspNetCore.Testing; using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; -using System; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -34,10 +33,16 @@ protected override void InitializeAsyncCore() Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); } + protected virtual IWebElement MountSimpleValidationComponent() + => Browser.MountTestComponent(); + + protected virtual IWebElement MountTypicalValidationComponent() + => Browser.MountTestComponent(); + [Fact] public async Task EditFormWorksWithDataAnnotationsValidator() { - var appElement = Browser.MountTestComponent(); + var appElement = MountSimpleValidationComponent();; var form = appElement.FindElement(By.TagName("form")); var userNameInput = appElement.FindElement(By.ClassName("user-name")).FindElement(By.TagName("input")); var acceptsTermsInput = appElement.FindElement(By.ClassName("accepts-terms")).FindElement(By.TagName("input")); @@ -77,7 +82,7 @@ public async Task EditFormWorksWithDataAnnotationsValidator() [Fact] public void InputTextInteractsWithEditContext() { - var appElement = Browser.MountTestComponent(); + var appElement = MountTypicalValidationComponent(); var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input")); var messagesAccessor = CreateValidationMessagesAccessor(appElement); @@ -104,7 +109,7 @@ public void InputTextInteractsWithEditContext() [Fact] public void InputNumberInteractsWithEditContext_NonNullableInt() { - var appElement = Browser.MountTestComponent(); + var appElement = MountTypicalValidationComponent(); var ageInput = appElement.FindElement(By.ClassName("age")).FindElement(By.TagName("input")); var messagesAccessor = CreateValidationMessagesAccessor(appElement); @@ -136,7 +141,7 @@ public void InputNumberInteractsWithEditContext_NonNullableInt() [Fact] public void InputNumberInteractsWithEditContext_NullableFloat() { - var appElement = Browser.MountTestComponent(); + var appElement = MountTypicalValidationComponent(); var heightInput = appElement.FindElement(By.ClassName("height")).FindElement(By.TagName("input")); var messagesAccessor = CreateValidationMessagesAccessor(appElement); @@ -160,7 +165,7 @@ public void InputNumberInteractsWithEditContext_NullableFloat() [Fact] public void InputTextAreaInteractsWithEditContext() { - var appElement = Browser.MountTestComponent(); + var appElement = MountTypicalValidationComponent(); var descriptionInput = appElement.FindElement(By.ClassName("description")).FindElement(By.TagName("textarea")); var messagesAccessor = CreateValidationMessagesAccessor(appElement); @@ -187,7 +192,7 @@ public void InputTextAreaInteractsWithEditContext() [Fact] public void InputDateInteractsWithEditContext_NonNullableDateTime() { - var appElement = Browser.MountTestComponent(); + var appElement = MountTypicalValidationComponent(); var renewalDateInput = appElement.FindElement(By.ClassName("renewal-date")).FindElement(By.TagName("input")); var messagesAccessor = CreateValidationMessagesAccessor(appElement); @@ -218,7 +223,7 @@ public void InputDateInteractsWithEditContext_NonNullableDateTime() [Fact] public void InputDateInteractsWithEditContext_NullableDateTimeOffset() { - var appElement = Browser.MountTestComponent(); + var appElement = MountTypicalValidationComponent(); var expiryDateInput = appElement.FindElement(By.ClassName("expiry-date")).FindElement(By.TagName("input")); var messagesAccessor = CreateValidationMessagesAccessor(appElement); @@ -241,7 +246,7 @@ public void InputDateInteractsWithEditContext_NullableDateTimeOffset() [Fact] public void InputSelectInteractsWithEditContext() { - var appElement = Browser.MountTestComponent(); + var appElement = MountTypicalValidationComponent(); var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select"))); var select = ticketClassInput.WrappedElement; var messagesAccessor = CreateValidationMessagesAccessor(appElement); @@ -263,7 +268,7 @@ public void InputSelectInteractsWithEditContext() [Fact] public void InputCheckboxInteractsWithEditContext() { - var appElement = Browser.MountTestComponent(); + var appElement = MountTypicalValidationComponent(); var acceptsTermsInput = appElement.FindElement(By.ClassName("accepts-terms")).FindElement(By.TagName("input")); var isEvilInput = appElement.FindElement(By.ClassName("is-evil")).FindElement(By.TagName("input")); var messagesAccessor = CreateValidationMessagesAccessor(appElement); @@ -331,7 +336,7 @@ public void CanWireUpINotifyPropertyChangedToEditContext() [Fact] public void ValidationMessageDisplaysMessagesForField() { - var appElement = Browser.MountTestComponent(); + var appElement = MountTypicalValidationComponent(); var emailContainer = appElement.FindElement(By.ClassName("email")); var emailInput = emailContainer.FindElement(By.TagName("input")); var emailMessagesAccessor = CreateValidationMessagesAccessor(emailContainer); @@ -355,10 +360,43 @@ public void ValidationMessageDisplaysMessagesForField() Browser.Empty(emailMessagesAccessor); } + [Fact] + public void ErrorsFromCompareAttribute() + { + var appElement = MountTypicalValidationComponent(); + var emailContainer = appElement.FindElement(By.ClassName("email")); + var emailInput = emailContainer.FindElement(By.TagName("input")); + var confirmEmailContainer = appElement.FindElement(By.ClassName("confirm-email")); + var confirmInput = confirmEmailContainer.FindElement(By.TagName("input")); + var confirmEmailValidationMessage = CreateValidationMessagesAccessor(confirmEmailContainer); + var modelErrors = CreateValidationMessagesAccessor(appElement.FindElement(By.ClassName("model-errors"))); + CreateValidationMessagesAccessor(emailContainer); + var submitButton = appElement.FindElement(By.TagName("button")); + + // Updates on edit + emailInput.SendKeys("a@b.com\t"); + + submitButton.Click(); + Browser.Empty(confirmEmailValidationMessage); + Browser.Equal(new[] { "Email and confirm email do not match." }, modelErrors); + + confirmInput.SendKeys("not-test@example.com\t"); + Browser.Equal(new[] { "Email and confirm email do not match." }, confirmEmailValidationMessage); + + // Can become correct + confirmInput.Clear(); + confirmInput.SendKeys("a@b.com\t"); + + Browser.Empty(confirmEmailValidationMessage); + + submitButton.Click(); + Browser.Empty(modelErrors); + } + [Fact] public void InputComponentsCauseContainerToRerenderOnChange() { - var appElement = Browser.MountTestComponent(); + var appElement = MountTypicalValidationComponent(); var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select"))); var selectedTicketClassDisplay = appElement.FindElement(By.Id("selected-ticket-class")); var messagesAccessor = CreateValidationMessagesAccessor(appElement); diff --git a/src/Components/test/E2ETest/Tests/FormsTestWithExperimentalValiator.cs b/src/Components/test/E2ETest/Tests/FormsTestWithExperimentalValiator.cs new file mode 100644 index 000000000000..dc2fb06b3324 --- /dev/null +++ b/src/Components/test/E2ETest/Tests/FormsTestWithExperimentalValiator.cs @@ -0,0 +1,90 @@ +// 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 BasicTestApp.FormsTest; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using OpenQA.Selenium.Support.UI; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETest.Tests +{ + public class FormsTestWithExperimentalValidator : FormsTest + { + public FormsTestWithExperimentalValidator( + BrowserFixture browserFixture, + ToggleExecutionModeServerFixture serverFixture, + ITestOutputHelper output) : base(browserFixture, serverFixture, output) + { + } + + protected override IWebElement MountSimpleValidationComponent() + => Browser.MountTestComponent(); + + protected override IWebElement MountTypicalValidationComponent() + => Browser.MountTestComponent(); + + [Fact] + public void EditFormWorksWithNestedValidation() + { + var appElement = Browser.MountTestComponent(); + + var nameInput = appElement.FindElement(By.CssSelector(".name input")); + var emailInput = appElement.FindElement(By.CssSelector(".email input")); + var confirmEmailInput = appElement.FindElement(By.CssSelector(".confirm-email input")); + var streetInput = appElement.FindElement(By.CssSelector(".street input")); + var zipInput = appElement.FindElement(By.CssSelector(".zip input")); + var countryInput = new SelectElement(appElement.FindElement(By.CssSelector(".country select"))); + var descriptionInput = appElement.FindElement(By.CssSelector(".description input")); + var weightInput = appElement.FindElement(By.CssSelector(".weight input")); + + var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]")); + + submitButton.Click(); + + Browser.Equal(4, () => appElement.FindElements(By.CssSelector(".all-errors .validation-message")).Count); + + Browser.Equal("Enter a name", () => appElement.FindElement(By.CssSelector(".name .validation-message")).Text); + Browser.Equal("Enter an email", () => appElement.FindElement(By.CssSelector(".email .validation-message")).Text); + Browser.Equal("A street address is required.", () => appElement.FindElement(By.CssSelector(".street .validation-message")).Text); + Browser.Equal("Description is required.", () => appElement.FindElement(By.CssSelector(".description .validation-message")).Text); + + // Verify class-level validation + nameInput.SendKeys("Some person"); + emailInput.SendKeys("test@example.com"); + countryInput.SelectByValue("Mordor"); + descriptionInput.SendKeys("Fragile staff"); + streetInput.SendKeys("Mount Doom\t"); + + submitButton.Click(); + + // Verify member validation from IValidatableObject on a model property, CustomValidationAttribute on a model attribute, and BlazorCompareAttribute. + Browser.Equal("A ZipCode is required", () => appElement.FindElement(By.CssSelector(".zip .validation-message")).Text); + Browser.Equal("'Confirm email address' and 'EmailAddress' do not match.", () => appElement.FindElement(By.CssSelector(".confirm-email .validation-message")).Text); + Browser.Equal("Fragile items must be placed in secure containers", () => appElement.FindElement(By.CssSelector(".item-error .validation-message")).Text); + Browser.Equal(3, () => appElement.FindElements(By.CssSelector(".all-errors .validation-message")).Count); + + zipInput.SendKeys("98052"); + confirmEmailInput.SendKeys("test@example.com"); + descriptionInput.Clear(); + weightInput.SendKeys("0"); + descriptionInput.SendKeys("The One Ring\t"); + + submitButton.Click(); + // Verify validation from IValidatableObject on the model. + Browser.Equal("Some items in your list cannot be delivered.", () => appElement.FindElement(By.CssSelector(".model-errors .validation-message")).Text); + + Browser.Single(() => appElement.FindElements(By.CssSelector(".all-errors .validation-message"))); + + // Let's make sure the form submits + descriptionInput.Clear(); + descriptionInput.SendKeys("A different ring\t"); + submitButton.Click(); + + Browser.Empty(() => appElement.FindElements(By.CssSelector(".all-errors .validation-message"))); + Browser.Equal("OnValidSubmit", () => appElement.FindElement(By.CssSelector(".submission-log")).Text); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj index ac0e53051620..98357d0e8835 100644 --- a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj +++ b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/ExperimentalValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/ExperimentalValidationComponent.razor new file mode 100644 index 000000000000..dd574f4dbe89 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/ExperimentalValidationComponent.razor @@ -0,0 +1,181 @@ +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Components.Forms + + + + + +

    + Name: + +

    + + + + + +
    + Items to deliver +

    + +

    +
      + @foreach (var item in model.Items) + { +
    • +
      +
      + + +
      + +
      + + +
      +
      + +
      +
      +
    • + } +
    +
    + +
    + Shipping details +

    + Street Address: + +

    +

    + Zip Code: + +

    +

    + Country: + + + + + + + + +

    +

    + +

    +
    + +
    + +
    + + +
    + +
    + +
    + +
      + @foreach (var entry in submissionLog) + { +
    • @entry
    • + } +
    + +@code { + Delivery model = new Delivery(); + + public class Delivery : IValidatableObject + { + [Required(ErrorMessage = "Enter a name")] + public string Recipient { get; set; } + + [Required(ErrorMessage = "Enter an email")] + [EmailAddress(ErrorMessage = "Enter a valid email address")] + public string EmailAddress { get; set; } + + [BlazorCompare(nameof(EmailAddress))] + [Display(Name = "Confirm email address")] + public string ConfirmEmailAddress { get; set; } + + [ValidateComplexType] + public Address Address { get; } = new Address(); + + [ValidateComplexType] + public List Items { get; } = new List + { + new Item(), + }; + + public IEnumerable Validate(ValidationContext context) + { + if (Address.Street == "Mount Doom" && Items.Any(i => i.Description == "The One Ring")) + { + yield return new ValidationResult("Some items in your list cannot be delivered."); + } + } + } + + public class Address : IValidatableObject + { + [Required(ErrorMessage = "A street address is required.")] + public string Street { get; set; } + + public string ZipCode { get; set; } + + [EnumDataType(typeof(Country))] + public Country Country { get; set; } + + public IEnumerable Validate(ValidationContext context) + { + if (Country == Country.Mordor && string.IsNullOrEmpty(ZipCode)) + { + yield return new ValidationResult("A ZipCode is required", new[] { nameof(ZipCode) }); + } + } + } + + [CustomValidation(typeof(Item), nameof(Item.CustomValidate))] + public class Item + { + [Required(ErrorMessage = "Description is required.")] + public string Description { get; set; } + + [Range(0.1, 50, ErrorMessage = "Items must weigh between 0.1 and 5")] + public double Weight { get; set; } = 1; + + public static ValidationResult CustomValidate(Item item, ValidationContext context) + { + if (item.Weight < 2.0 && item.Description.StartsWith("Fragile")) + { + return new ValidationResult("Fragile items must be placed in secure containers"); + } + + return ValidationResult.Success; + } + } + + public enum Country { Gondor, Mordor, Rohan, Shire } + + List submissionLog = new List(); + + void HandleValidSubmit() + { + submissionLog.Add("OnValidSubmit"); + } + + void AddItem() + { + model.Items.Add(new Item()); + } +} diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponent.razor index aa8ccfbe54a9..d4f8ce464fbd 100644 --- a/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponent.razor @@ -2,7 +2,14 @@ @using Microsoft.AspNetCore.Components.Forms - + @if (UseExperimentalValidator) + { + + } + else + { + + }

    User name: @@ -29,6 +36,8 @@ } @code { + protected virtual bool UseExperimentalValidator => false; + string lastCallback; [Required(ErrorMessage = "Please choose a username")] diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponentUsingExperimentalValidator.cs b/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponentUsingExperimentalValidator.cs new file mode 100644 index 000000000000..90484aad05d0 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponentUsingExperimentalValidator.cs @@ -0,0 +1,7 @@ +namespace BasicTestApp.FormsTest +{ + public class TypicalValidationComponentUsingExperimentalValidator : TypicalValidationComponent + { + protected override bool UseExperimentalValidator => true; + } +} diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor index 1bd3b748d100..be2abd5e7919 100644 --- a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor @@ -2,7 +2,14 @@ @using Microsoft.AspNetCore.Components.Forms + @if (UseExperimentalValidator) + { + + } + else + { + }

    Name: @@ -11,6 +18,10 @@ Email:

    +

    Age (years):

    @@ -49,12 +60,18 @@ +

    + +

    +
      @foreach (var entry in submissionLog) {
    • @entry
    • }
    @code { + protected virtual bool UseExperimentalValidator => false; + Person person = new Person(); EditContext editContext; ValidationMessageStore customValidationMessageStore; @@ -75,6 +92,9 @@ [StringLength(10, ErrorMessage = "We only accept very short email addresses (max 10 chars)")] public string Email { get; set; } + [Compare(nameof(Email), ErrorMessage = "Email and confirm email do not match.")] + public string ConfirmEmail { get; set; } + [Range(0, 200, ErrorMessage = "Nobody is that old")] public int AgeInYears { get; set; } diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponentUsingExperimentalValidator.cs b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponentUsingExperimentalValidator.cs new file mode 100644 index 000000000000..9ede005a8aa6 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponentUsingExperimentalValidator.cs @@ -0,0 +1,7 @@ +namespace BasicTestApp.FormsTest +{ + public class SimpleValidationComponentUsingExperimentalValidator : SimpleValidationComponent + { + protected override bool UseExperimentalValidator => true; + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index fe94ad595e45..f836fa6c390a 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -29,7 +29,10 @@ + + + diff --git a/src/Shared/E2ETesting/BrowserAssertFailedException.cs b/src/Shared/E2ETesting/BrowserAssertFailedException.cs index 007db0ac7965..08d04a912d5f 100644 --- a/src/Shared/E2ETesting/BrowserAssertFailedException.cs +++ b/src/Shared/E2ETesting/BrowserAssertFailedException.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using Xunit.Sdk; namespace OpenQA.Selenium @@ -13,15 +14,31 @@ namespace OpenQA.Selenium // case. public class BrowserAssertFailedException : XunitException { - public BrowserAssertFailedException(IReadOnlyList logs, Exception innerException, string screenShotPath) - : base(BuildMessage(innerException, logs, screenShotPath), innerException) + public BrowserAssertFailedException(IReadOnlyList logs, Exception innerException, string screenShotPath, string innerHTML) + : base(BuildMessage(innerException, logs, screenShotPath, innerHTML), innerException) { } - private static string BuildMessage(Exception innerException, IReadOnlyList logs, string screenShotPath) => - innerException.ToString() + Environment.NewLine + - (File.Exists(screenShotPath) ? $"Screen shot captured at '{screenShotPath}'" + Environment.NewLine : "") + - (logs.Count > 0 ? "Encountered browser logs" : "No browser logs found") + " while running the assertion." + Environment.NewLine + - string.Join(Environment.NewLine, logs); + private static string BuildMessage(Exception exception, IReadOnlyList logs, string screenShotPath, string innerHTML) + { + var builder = new StringBuilder(); + builder.AppendLine(exception.ToString()); + + if (File.Exists(screenShotPath)) + { + builder.AppendLine($"Screen shot captured at '{screenShotPath}'"); + } + + if (logs.Count > 0) + { + builder.AppendLine("Encountered browser errors") + .AppendJoin(Environment.NewLine, logs); + } + + builder.AppendLine("Page content:") + .AppendLine(innerHTML); + + return builder.ToString(); + } } } diff --git a/src/Shared/E2ETesting/WaitAssert.cs b/src/Shared/E2ETesting/WaitAssert.cs index 4ef144619137..8792d2692f9f 100644 --- a/src/Shared/E2ETesting/WaitAssert.cs +++ b/src/Shared/E2ETesting/WaitAssert.cs @@ -101,6 +101,8 @@ private static TResult WaitAssertCore(IWebDriver driver, Func // tests running concurrently might use the DefaultTimeout in their current assertion, which is fine. TestRunFailed = true; + var innerHtml = driver.FindElement(By.CssSelector(":first-child")).GetAttribute("innerHTML"); + var fileId = $"{Guid.NewGuid():N}.png"; var screenShotPath = Path.Combine(Path.GetFullPath(E2ETestOptions.Instance.ScreenShotsPath), fileId); var errors = driver.GetBrowserLogs(LogLevel.All); @@ -109,7 +111,7 @@ private static TResult WaitAssertCore(IWebDriver driver, Func var exceptionInfo = lastException != null ? ExceptionDispatchInfo.Capture(lastException) : CaptureException(() => assertion()); - throw new BrowserAssertFailedException(errors, exceptionInfo.SourceException, screenShotPath); + throw new BrowserAssertFailedException(errors, exceptionInfo.SourceException, screenShotPath, innerHtml); } return result; From 75a863305ff31737e5099887ae915e9b6e21c5b3 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 14 Oct 2019 12:20:43 -0700 Subject: [PATCH 2/9] fixup --- eng/ProjectReferences.props | 2 +- .../Blazor/Validation/test/BlazorDatAnnotationsValidatorTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 37704e933ee5..16f0ef714a24 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -137,7 +137,7 @@ - + diff --git a/src/Components/Blazor/Validation/test/BlazorDatAnnotationsValidatorTest.cs b/src/Components/Blazor/Validation/test/BlazorDatAnnotationsValidatorTest.cs index 4cea91d1161b..7877707170eb 100644 --- a/src/Components/Blazor/Validation/test/BlazorDatAnnotationsValidatorTest.cs +++ b/src/Components/Blazor/Validation/test/BlazorDatAnnotationsValidatorTest.cs @@ -171,7 +171,7 @@ public void ValidateObject_ValidatableComplexProperty_ValidatesIValidatablePrope var messages = editContext.GetValidationMessages(() => model.Property1); Assert.Single(messages); - messages = editContext.GetValidationMessages(() => model.Property2); + messages = editContext.GetValidationMessages(new FieldIdentifier(model.Property2, string.Empty)); Assert.Single(messages); messages = editContext.GetValidationMessages(() => model.Property2.Name); From 75fde42ce3dde3122ea819a9fd5fba7d0e080362 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 14 Oct 2019 13:13:27 -0700 Subject: [PATCH 3/9] Update ref asm --- .../Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs | 2 ++ .../ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs b/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs index cb686083cd9c..534580160f21 100644 --- a/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs +++ b/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs @@ -125,6 +125,8 @@ public partial class ValidationSummary : Microsoft.AspNetCore.Components.Compone public ValidationSummary() { } [Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)] public System.Collections.Generic.IReadOnlyDictionary AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public object Model { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } protected virtual void Dispose(bool disposing) { } protected override void OnParametersSet() { } diff --git a/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs b/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs index cb686083cd9c..534580160f21 100644 --- a/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs +++ b/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs @@ -125,6 +125,8 @@ public partial class ValidationSummary : Microsoft.AspNetCore.Components.Compone public ValidationSummary() { } [Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)] public System.Collections.Generic.IReadOnlyDictionary AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public object Model { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } protected virtual void Dispose(bool disposing) { } protected override void OnParametersSet() { } From 44de03650fbab1fcebd606cc72b846a0900235ea Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 16 Oct 2019 12:40:44 -0700 Subject: [PATCH 4/9] Changes per PR --- ...tribute.cs => ComparePropertyAttribute.cs} | 5 +- ...e.Blazor.DataAnnotations.Validation.csproj | 2 +- ...=> ObjectGraphDataAnnotationsValidator.cs} | 25 ++-- .../src/ValidateComplexTypeAttribute.cs | 9 +- ...bjectGraphDataAnnotationsValidatorTest.cs} | 127 +++++++++++++++++- src/Components/Components.sln | 4 +- src/Components/ComponentsNoDeps.slnf | 4 +- .../Web.JS/dist/Release/blazor.server.js | 2 +- .../test/E2ETest/Tests/FormsTest.cs | 8 +- ... => FormsTestWithExperimentalValidator.cs} | 0 .../ExperimentalValidationComponent.razor | 8 +- .../FormsTest/SimpleValidationComponent.razor | 2 +- .../TypicalValidationComponent.razor | 2 +- 13 files changed, 166 insertions(+), 32 deletions(-) rename src/Components/Blazor/Validation/src/{BlazorCompareAttribute.cs => ComparePropertyAttribute.cs} (84%) rename src/Components/Blazor/Validation/src/{BlazorDataAnnotationsValidator.cs => ObjectGraphDataAnnotationsValidator.cs} (80%) rename src/Components/Blazor/Validation/test/{BlazorDatAnnotationsValidatorTest.cs => ObjectGraphDataAnnotationsValidatorTest.cs} (76%) rename src/Components/test/E2ETest/Tests/{FormsTestWithExperimentalValiator.cs => FormsTestWithExperimentalValidator.cs} (100%) diff --git a/src/Components/Blazor/Validation/src/BlazorCompareAttribute.cs b/src/Components/Blazor/Validation/src/ComparePropertyAttribute.cs similarity index 84% rename from src/Components/Blazor/Validation/src/BlazorCompareAttribute.cs rename to src/Components/Blazor/Validation/src/ComparePropertyAttribute.cs index f180b1b47936..3f74ce647f37 100644 --- a/src/Components/Blazor/Validation/src/BlazorCompareAttribute.cs +++ b/src/Components/Blazor/Validation/src/ComparePropertyAttribute.cs @@ -6,13 +6,14 @@ namespace System.ComponentModel.DataAnnotations /// /// A that compares two properties /// - public sealed class BlazorCompareAttribute : CompareAttribute + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public sealed class ComparePropertyAttribute : CompareAttribute { /// /// Initializes a new instance of . /// /// The property to compare with the current property. - public BlazorCompareAttribute(string otherProperty) + public ComparePropertyAttribute(string otherProperty) : base(otherProperty) { } diff --git a/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj b/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj index c7621001175d..2d73d62df1b1 100644 --- a/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj +++ b/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj @@ -2,7 +2,7 @@ netstandard2.0 - Provides experimental support for validation of complex properties using DataAnnotations. + Provides experimental support for validation using DataAnnotations. true diff --git a/src/Components/Blazor/Validation/src/BlazorDataAnnotationsValidator.cs b/src/Components/Blazor/Validation/src/ObjectGraphDataAnnotationsValidator.cs similarity index 80% rename from src/Components/Blazor/Validation/src/BlazorDataAnnotationsValidator.cs rename to src/Components/Blazor/Validation/src/ObjectGraphDataAnnotationsValidator.cs index 27a8ef4a9327..df1971e0a200 100644 --- a/src/Components/Blazor/Validation/src/BlazorDataAnnotationsValidator.cs +++ b/src/Components/Blazor/Validation/src/ObjectGraphDataAnnotationsValidator.cs @@ -7,9 +7,10 @@ namespace Microsoft.AspNetCore.Components.Forms { - public class BlazorDataAnnotationsValidator : ComponentBase + public class ObjectGraphDataAnnotationsValidator : ComponentBase { private static readonly object ValidationContextValidatorKey = new object(); + private static readonly object ValidatedObjectsKey = new object(); private ValidationMessageStore _validationMessageStore; [CascadingParameter] @@ -23,7 +24,7 @@ protected override void OnInitialized() EditContext.OnValidationRequested += (sender, eventArgs) => { _validationMessageStore.Clear(); - ValidateObject(EditContext.Model); + ValidateObject(EditContext.Model, new HashSet()); EditContext.NotifyValidationStateChanged(); }; @@ -32,19 +33,25 @@ protected override void OnInitialized() ValidateField(EditContext, _validationMessageStore, eventArgs.FieldIdentifier); } - internal void ValidateObject(object value) + internal void ValidateObject(object value, HashSet visited) { if (value is null) { return; } + if (!visited.Add(value)) + { + // Already visited this object. + return; + } + if (value is IEnumerable enumerable) { var index = 0; foreach (var item in enumerable) { - ValidateObject(item); + ValidateObject(item, visited); index++; } @@ -52,7 +59,7 @@ internal void ValidateObject(object value) } var validationResults = new List(); - ValidateObject(value, validationResults); + ValidateObject(value, visited, validationResults); // Transfer results to the ValidationMessageStore foreach (var validationResult in validationResults) @@ -71,18 +78,20 @@ internal void ValidateObject(object value) } } - private void ValidateObject(object value, List validationResults) + private void ValidateObject(object value, HashSet visited, List validationResults) { var validationContext = new ValidationContext(value); validationContext.Items.Add(ValidationContextValidatorKey, this); + validationContext.Items.Add(ValidatedObjectsKey, visited); 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) + if (validationContext.Items.TryGetValue(ValidationContextValidatorKey, out var result) && result is ObjectGraphDataAnnotationsValidator validator) { - validator.ValidateObject(value); + var visited = (HashSet)validationContext.Items[ValidatedObjectsKey]; + validator.ValidateObject(value, visited); return true; } diff --git a/src/Components/Blazor/Validation/src/ValidateComplexTypeAttribute.cs b/src/Components/Blazor/Validation/src/ValidateComplexTypeAttribute.cs index 9203621d13a5..4769d84767ad 100644 --- a/src/Components/Blazor/Validation/src/ValidateComplexTypeAttribute.cs +++ b/src/Components/Blazor/Validation/src/ValidateComplexTypeAttribute.cs @@ -8,8 +8,9 @@ namespace System.ComponentModel.DataAnnotations /// /// A that indicates that the property is a complex or collection type that further needs to be validated. /// - /// By default does not recurse in to complex property types during validation. When used in conjunction with , - /// this property allows the validation system to validate complex or collection type properties. + /// By default does not recurse in to complex property types during validation. + /// When used in conjunction with , this property allows the validation system to validate + /// complex or collection type properties. /// /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] @@ -18,9 +19,9 @@ public sealed class ValidateComplexTypeAttribute : ValidationAttribute /// protected override ValidationResult IsValid(object value, ValidationContext validationContext) { - if (!BlazorDataAnnotationsValidator.TryValidateRecursive(value, validationContext)) + if (!ObjectGraphDataAnnotationsValidator.TryValidateRecursive(value, validationContext)) { - throw new InvalidOperationException($"{nameof(ValidateComplexTypeAttribute)} can only used with {nameof(BlazorDataAnnotationsValidator)}."); + throw new InvalidOperationException($"{nameof(ValidateComplexTypeAttribute)} can only used with {nameof(ObjectGraphDataAnnotationsValidator)}."); } return ValidationResult.Success; diff --git a/src/Components/Blazor/Validation/test/BlazorDatAnnotationsValidatorTest.cs b/src/Components/Blazor/Validation/test/ObjectGraphDataAnnotationsValidatorTest.cs similarity index 76% rename from src/Components/Blazor/Validation/test/BlazorDatAnnotationsValidatorTest.cs rename to src/Components/Blazor/Validation/test/ObjectGraphDataAnnotationsValidatorTest.cs index 7877707170eb..6703eb35d540 100644 --- a/src/Components/Blazor/Validation/test/BlazorDatAnnotationsValidatorTest.cs +++ b/src/Components/Blazor/Validation/test/ObjectGraphDataAnnotationsValidatorTest.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components { - public class BlazorDatAnnotationsValidatorTest + public class ObjectGraphDataAnnotationsValidatorTest { public class SimpleModel { @@ -306,6 +306,125 @@ public void ValidateObject_ManyLevels() Assert.Equal(2, editContext.GetValidationMessages().Count()); } + private class Person + { + [Required] + public string Name { get; set; } + + [ValidateComplexType] + public Person Related { get; set; } + } + + [Fact] + public void ValidateObject_RecursiveRelation() + { + var model = new Person { Related = new Person() }; + model.Related.Related = model; + + var editContext = Validate(model); + + var messages = editContext.GetValidationMessages(() => model.Name); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => model.Related.Name); + Assert.Single(messages); + + Assert.Equal(2, editContext.GetValidationMessages().Count()); + } + + [Fact] + public void ValidateObject_RecursiveRelation_OverManySteps() + { + var person1 = new Person(); + var person2 = new Person { Name = "Valid name" }; + var person3 = new Person(); + var person4 = new Person(); + + person1.Related = person2; + person2.Related = person3; + person3.Related = person4; + person4.Related = person1; + + var editContext = Validate(person1); + + var messages = editContext.GetValidationMessages(() => person1.Name); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => person2.Name); + Assert.Empty(messages); + + messages = editContext.GetValidationMessages(() => person3.Name); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => person4.Name); + Assert.Single(messages); + + Assert.Equal(3, editContext.GetValidationMessages().Count()); + } + + private class Node + { + [Required] + public string Id { get; set; } + + [ValidateComplexType] + public List Related { get; set; } = new List(); + } + + [Fact] + public void ValidateObject_RecursiveRelation_ViaCollection() + { + var node1 = new Node(); + var node2 = new Node { Id = "Valid Id" }; + var node3 = new Node(); + node1.Related.Add(node2); + node2.Related.Add(node3); + node3.Related.Add(node1); + + var editContext = Validate(node1); + + var messages = editContext.GetValidationMessages(() => node1.Id); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => node2.Id); + Assert.Empty(messages); + + messages = editContext.GetValidationMessages(() => node3.Id); + Assert.Single(messages); + + Assert.Equal(2, editContext.GetValidationMessages().Count()); + } + + [Fact] + public void ValidateObject_RecursiveRelation_InCollection() + { + var person1 = new Person(); + var person2 = new Person { Name = "Valid name" }; + var person3 = new Person(); + var person4 = new Person(); + + person1.Related = person2; + person2.Related = person3; + person3.Related = person4; + person4.Related = person1; + + var editContext = Validate(person1); + + var messages = editContext.GetValidationMessages(() => person1.Name); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => person2.Name); + Assert.Empty(messages); + + messages = editContext.GetValidationMessages(() => person3.Name); + Assert.Single(messages); + + messages = editContext.GetValidationMessages(() => person4.Name); + Assert.Single(messages); + + Assert.Equal(3, editContext.GetValidationMessages().Count()); + } + [Fact] public void ValidateField_PropertyValid() { @@ -394,7 +513,7 @@ public void ValidateField_ModelWithComplexProperty_AfterSubmitValidation() private static EditContext Validate(object model) { var editContext = new EditContext(model); - var validator = new TestBlazorDataAnnotationsValidator { EditContext = editContext, }; + var validator = new TestObjectGraphDataAnnotationsValidator { EditContext = editContext, }; validator.OnInitialized(); editContext.Validate(); @@ -405,7 +524,7 @@ private static EditContext Validate(object model) private static EditContext ValidateField(object model, in FieldIdentifier field) { var editContext = new EditContext(model); - var validator = new TestBlazorDataAnnotationsValidator { EditContext = editContext, }; + var validator = new TestObjectGraphDataAnnotationsValidator { EditContext = editContext, }; validator.OnInitialized(); editContext.NotifyFieldChanged(field); @@ -413,7 +532,7 @@ private static EditContext ValidateField(object model, in FieldIdentifier field) return editContext; } - private class TestBlazorDataAnnotationsValidator : BlazorDataAnnotationsValidator + private class TestObjectGraphDataAnnotationsValidator : ObjectGraphDataAnnotationsValidator { public new void OnInitialized() => base.OnInitialized(); } diff --git a/src/Components/Components.sln b/src/Components/Components.sln index 3fcc3175b512..ba0b2476ffb3 100644 --- a/src/Components/Components.sln +++ b/src/Components/Components.sln @@ -1627,8 +1627,8 @@ Global {CD0EF85C-4187-4515-A355-E5A0D4485F40} = {BDE2397D-C53A-4783-8B3A-1F54F48A6926} {F31E8118-014E-4CCE-8A48-5282F7B9BB3E} = {BDE2397D-C53A-4783-8B3A-1F54F48A6926} {FD9BD646-9D50-42ED-A3E1-90558BA0C6B2} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} - {B70F90C7-2696-4050-B24E-BF0308F4E059} = {FD9BD646-9D50-42ED-A3E1-90558BA0C6B2} - {A5617A9D-C71E-44DE-936C-27611EB40A02} = {FD9BD646-9D50-42ED-A3E1-90558BA0C6B2} + {B70F90C7-2696-4050-B24E-BF0308F4E059} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} + {A5617A9D-C71E-44DE-936C-27611EB40A02} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CC3C47E1-AD1A-4619-9CD3-E08A0148E5CE} diff --git a/src/Components/ComponentsNoDeps.slnf b/src/Components/ComponentsNoDeps.slnf index 715ea1d4fd0c..09f6a0859f95 100644 --- a/src/Components/ComponentsNoDeps.slnf +++ b/src/Components/ComponentsNoDeps.slnf @@ -13,10 +13,10 @@ "Blazor\\DevServer\\src\\Microsoft.AspNetCore.Blazor.DevServer.csproj", "Blazor\\Http\\src\\Microsoft.AspNetCore.Blazor.HttpClient.csproj", "Blazor\\Http\\test\\Microsoft.AspNetCore.Blazor.HttpClient.Tests.csproj", - "Blazor\\Validation\\src\\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj", - "Blazor\\Validation\\test\\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj", "Blazor\\Server\\src\\Microsoft.AspNetCore.Blazor.Server.csproj", "Blazor\\Templates\\src\\Microsoft.AspNetCore.Blazor.Templates.csproj", + "Blazor\\Validation\\src\\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj", + "Blazor\\Validation\\test\\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj", "Blazor\\testassets\\HostedInAspNet.Client\\HostedInAspNet.Client.csproj", "Blazor\\testassets\\HostedInAspNet.Server\\HostedInAspNet.Server.csproj", "Blazor\\testassets\\Microsoft.AspNetCore.Blazor.E2EPerformance\\Microsoft.AspNetCore.Blazor.E2EPerformance.csproj", diff --git a/src/Components/Web.JS/dist/Release/blazor.server.js b/src/Components/Web.JS/dist/Release/blazor.server.js index 3b085c3f0c7d..3a2abaced976 100644 --- a/src/Components/Web.JS/dist/Release/blazor.server.js +++ b/src/Components/Web.JS/dist/Release/blazor.server.js @@ -1,4 +1,4 @@ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=50)}([function(e,t,n){"use strict";var r;n.d(t,"a",function(){return r}),function(e){e[e.Trace=0]="Trace",e[e.Debug=1]="Debug",e[e.Information=2]="Information",e[e.Warning=3]="Warning",e[e.Error=4]="Error",e[e.Critical=5]="Critical",e[e.None=6]="None"}(r||(r={}))},function(e,t,n){"use strict";n.d(t,"a",function(){return s}),n.d(t,"c",function(){return c}),n.d(t,"f",function(){return u}),n.d(t,"g",function(){return l}),n.d(t,"h",function(){return f}),n.d(t,"e",function(){return h}),n.d(t,"d",function(){return p}),n.d(t,"b",function(){return d});var r=n(0),o=n(7),i=function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{c(r.next(e))}catch(e){i(e)}}function s(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}c((r=r.apply(e,t||[])).next())})},a=function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]-1&&this.subject.observers.splice(e,1),0===this.subject.observers.length&&this.subject.cancelCallback&&this.subject.cancelCallback().catch(function(e){})},e}(),d=function(){function e(e){this.minimumLogLevel=e,this.outputConsole=console}return e.prototype.log=function(e,t){if(e>=this.minimumLogLevel)switch(e){case r.a.Critical:case r.a.Error:this.outputConsole.error("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;case r.a.Warning:this.outputConsole.warn("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;case r.a.Information:this.outputConsole.info("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;default:this.outputConsole.log("["+(new Date).toISOString()+"] "+r.a[e]+": "+t)}},e}()},function(e,t,n){"use strict";n.r(t);var r,o,i=n(3),a=n(4),s=n(44),c=n(0),u=(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),l=function(e){function t(t){var n=e.call(this)||this;return n.logger=t,n}return u(t,e),t.prototype.send=function(e){var t=this;return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new i.a):e.method?e.url?new Promise(function(n,r){var o=new XMLHttpRequest;o.open(e.method,e.url,!0),o.withCredentials=!0,o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.setRequestHeader("Content-Type","text/plain;charset=UTF-8");var s=e.headers;s&&Object.keys(s).forEach(function(e){o.setRequestHeader(e,s[e])}),e.responseType&&(o.responseType=e.responseType),e.abortSignal&&(e.abortSignal.onabort=function(){o.abort(),r(new i.a)}),e.timeout&&(o.timeout=e.timeout),o.onload=function(){e.abortSignal&&(e.abortSignal.onabort=null),o.status>=200&&o.status<300?n(new a.b(o.status,o.statusText,o.response||o.responseText)):r(new i.b(o.statusText,o.status))},o.onerror=function(){t.logger.log(c.a.Warning,"Error from HTTP request. "+o.status+": "+o.statusText+"."),r(new i.b(o.statusText,o.status))},o.ontimeout=function(){t.logger.log(c.a.Warning,"Timeout from HTTP request."),r(new i.c)},o.send(e.content||"")}):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t}(a.a),f=function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),h=function(e){function t(t){var n=e.call(this)||this;return"undefined"!=typeof XMLHttpRequest?n.httpClient=new l(t):n.httpClient=new s.a(t),n}return f(t,e),t.prototype.send=function(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new i.a):e.method?e.url?this.httpClient.send(e):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t.prototype.getCookieString=function(e){return this.httpClient.getCookieString(e)},t}(a.a),p=n(45);!function(e){e[e.Invocation=1]="Invocation",e[e.StreamItem=2]="StreamItem",e[e.Completion=3]="Completion",e[e.StreamInvocation=4]="StreamInvocation",e[e.CancelInvocation=5]="CancelInvocation",e[e.Ping=6]="Ping",e[e.Close=7]="Close"}(o||(o={}));var d,g=n(1),y=function(){function e(){this.observers=[]}return e.prototype.next=function(e){for(var t=0,n=this.observers;t0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0?[2,Promise.reject(new Error("Unable to connect to the server with any of the available transports. "+i.join(" ")))]:[2,Promise.reject(new Error("None of the transports supported by the client are supported by the server."))]}})})},e.prototype.constructTransport=function(e){switch(e){case E.WebSockets:if(!this.options.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new A(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.WebSocket);case E.ServerSentEvents:if(!this.options.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new O(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.EventSource);case E.LongPolling:return new P(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1);default:throw new Error("Unknown transport: "+e+".")}},e.prototype.startTransport=function(e,t){var n=this;return this.transport.onreceive=this.onreceive,this.transport.onclose=function(e){return n.stopConnection(e)},this.transport.connect(e,t)},e.prototype.resolveTransportOrError=function(e,t,n){var r=E[e.transport];if(null==r)return this.logger.log(c.a.Debug,"Skipping transport '"+e.transport+"' because it is not supported by this client."),new Error("Skipping transport '"+e.transport+"' because it is not supported by this client.");if(!function(e,t){return!e||0!=(t&e)}(t,r))return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it was disabled by the client."),new Error("'"+E[r]+"' is disabled by the client.");if(!(e.transferFormats.map(function(e){return S[e]}).indexOf(n)>=0))return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it does not support the requested transfer format '"+S[n]+"'."),new Error("'"+E[r]+"' does not support "+S[n]+".");if(r===E.WebSockets&&!this.options.WebSocket||r===E.ServerSentEvents&&!this.options.EventSource)return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it is not supported in your environment.'"),new Error("'"+E[r]+"' is not supported in your environment.");this.logger.log(c.a.Debug,"Selecting transport '"+E[r]+"'.");try{return this.constructTransport(r)}catch(e){return e}},e.prototype.isITransport=function(e){return e&&"object"==typeof e&&"connect"in e},e.prototype.stopConnection=function(e){if(this.logger.log(c.a.Debug,"HttpConnection.stopConnection("+e+") called while in state "+this.connectionState+"."),this.transport=void 0,e=this.stopError||e,this.stopError=void 0,"Disconnected"!==this.connectionState)if("Connecting "!==this.connectionState){if("Disconnecting"===this.connectionState&&this.stopPromiseResolver(),e?this.logger.log(c.a.Error,"Connection disconnected with error '"+e+"'."):this.logger.log(c.a.Information,"Connection disconnected."),this.connectionId=void 0,this.connectionState="Disconnected",this.onclose&&this.connectionStarted){this.connectionStarted=!1;try{this.onclose(e)}catch(t){this.logger.log(c.a.Error,"HttpConnection.onclose("+e+") threw error '"+t+"'.")}}}else this.logger.log(c.a.Warning,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection hasn't yet left the in the connecting state.");else this.logger.log(c.a.Debug,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection is already in the disconnected state.")},e.prototype.resolveUrl=function(e){if(0===e.lastIndexOf("https://",0)||0===e.lastIndexOf("http://",0))return e;if(!g.c.isBrowser||!window.document)throw new Error("Cannot resolve '"+e+"'.");var t=window.document.createElement("a");return t.href=e,this.logger.log(c.a.Information,"Normalizing '"+e+"' to '"+t.href+"'."),t.href},e.prototype.resolveNegotiateUrl=function(e){var t=e.indexOf("?"),n=e.substring(0,-1===t?e.length:t);return"/"!==n[n.length-1]&&(n+="/"),n+="negotiate",-1===(n+=-1===t?"":e.substring(t)).indexOf("negotiateVersion")&&(n+=-1===t?"?":"&",n+="negotiateVersion="+this.negotiateVersion),n},e}();var q=function(){function e(e){this.transport=e,this.buffer=[],this.executing=!0,this.sendBufferedData=new W,this.transportResult=new W,this.sendLoopPromise=this.sendLoop()}return e.prototype.send=function(e){return this.bufferData(e),this.transportResult||(this.transportResult=new W),this.transportResult.promise},e.prototype.stop=function(){return this.executing=!1,this.sendBufferedData.resolve(),this.sendLoopPromise},e.prototype.bufferData=function(e){if(this.buffer.length&&typeof this.buffer[0]!=typeof e)throw new Error("Expected data to be of type "+typeof this.buffer+" but was of type "+typeof e);this.buffer.push(e),this.sendBufferedData.resolve()},e.prototype.sendLoop=function(){return B(this,void 0,void 0,function(){var t,n,r;return j(this,function(o){switch(o.label){case 0:return[4,this.sendBufferedData.promise];case 1:if(o.sent(),!this.executing)return this.transportResult&&this.transportResult.reject("Connection stopped."),[3,6];this.sendBufferedData=new W,t=this.transportResult,this.transportResult=void 0,n="string"==typeof this.buffer[0]?this.buffer.join(""):e.concatBuffers(this.buffer),this.buffer.length=0,o.label=2;case 2:return o.trys.push([2,4,,5]),[4,this.transport.send(n)];case 3:return o.sent(),t.resolve(),[3,5];case 4:return r=o.sent(),t.reject(r),[3,5];case 5:return[3,0];case 6:return[2]}})})},e.concatBuffers=function(e){for(var t=e.map(function(e){return e.byteLength}).reduce(function(e,t){return e+t}),n=new Uint8Array(t),r=0,o=0,i=e;o0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]-1&&this.subject.observers.splice(e,1),0===this.subject.observers.length&&this.subject.cancelCallback&&this.subject.cancelCallback().catch(function(e){})},e}(),d=function(){function e(e){this.minimumLogLevel=e,this.outputConsole=console}return e.prototype.log=function(e,t){if(e>=this.minimumLogLevel)switch(e){case r.a.Critical:case r.a.Error:this.outputConsole.error("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;case r.a.Warning:this.outputConsole.warn("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;case r.a.Information:this.outputConsole.info("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;default:this.outputConsole.log("["+(new Date).toISOString()+"] "+r.a[e]+": "+t)}},e}()},function(e,t,n){"use strict";n.r(t);var r,o,i=n(3),a=n(4),s=n(44),c=n(0),u=(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),l=function(e){function t(t){var n=e.call(this)||this;return n.logger=t,n}return u(t,e),t.prototype.send=function(e){var t=this;return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new i.a):e.method?e.url?new Promise(function(n,r){var o=new XMLHttpRequest;o.open(e.method,e.url,!0),o.withCredentials=!0,o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.setRequestHeader("Content-Type","text/plain;charset=UTF-8");var s=e.headers;s&&Object.keys(s).forEach(function(e){o.setRequestHeader(e,s[e])}),e.responseType&&(o.responseType=e.responseType),e.abortSignal&&(e.abortSignal.onabort=function(){o.abort(),r(new i.a)}),e.timeout&&(o.timeout=e.timeout),o.onload=function(){e.abortSignal&&(e.abortSignal.onabort=null),o.status>=200&&o.status<300?n(new a.b(o.status,o.statusText,o.response||o.responseText)):r(new i.b(o.statusText,o.status))},o.onerror=function(){t.logger.log(c.a.Warning,"Error from HTTP request. "+o.status+": "+o.statusText+"."),r(new i.b(o.statusText,o.status))},o.ontimeout=function(){t.logger.log(c.a.Warning,"Timeout from HTTP request."),r(new i.c)},o.send(e.content||"")}):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t}(a.a),f=function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),h=function(e){function t(t){var n=e.call(this)||this;return"undefined"!=typeof XMLHttpRequest?n.httpClient=new l(t):n.httpClient=new s.a(t),n}return f(t,e),t.prototype.send=function(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new i.a):e.method?e.url?this.httpClient.send(e):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t.prototype.getCookieString=function(e){return this.httpClient.getCookieString(e)},t}(a.a),p=n(45);!function(e){e[e.Invocation=1]="Invocation",e[e.StreamItem=2]="StreamItem",e[e.Completion=3]="Completion",e[e.StreamInvocation=4]="StreamInvocation",e[e.CancelInvocation=5]="CancelInvocation",e[e.Ping=6]="Ping",e[e.Close=7]="Close"}(o||(o={}));var d,g=n(1),y=function(){function e(){this.observers=[]}return e.prototype.next=function(e){for(var t=0,n=this.observers;t0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0?[2,Promise.reject(new Error("Unable to connect to the server with any of the available transports. "+i.join(" ")))]:[2,Promise.reject(new Error("None of the transports supported by the client are supported by the server."))]}})})},e.prototype.constructTransport=function(e){switch(e){case E.WebSockets:if(!this.options.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new A(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.WebSocket);case E.ServerSentEvents:if(!this.options.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new O(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.EventSource);case E.LongPolling:return new P(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1);default:throw new Error("Unknown transport: "+e+".")}},e.prototype.startTransport=function(e,t){var n=this;return this.transport.onreceive=this.onreceive,this.transport.onclose=function(e){return n.stopConnection(e)},this.transport.connect(e,t)},e.prototype.resolveTransportOrError=function(e,t,n){var r=E[e.transport];if(null==r)return this.logger.log(c.a.Debug,"Skipping transport '"+e.transport+"' because it is not supported by this client."),new Error("Skipping transport '"+e.transport+"' because it is not supported by this client.");if(!function(e,t){return!e||0!=(t&e)}(t,r))return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it was disabled by the client."),new Error("'"+E[r]+"' is disabled by the client.");if(!(e.transferFormats.map(function(e){return S[e]}).indexOf(n)>=0))return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it does not support the requested transfer format '"+S[n]+"'."),new Error("'"+E[r]+"' does not support "+S[n]+".");if(r===E.WebSockets&&!this.options.WebSocket||r===E.ServerSentEvents&&!this.options.EventSource)return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it is not supported in your environment.'"),new Error("'"+E[r]+"' is not supported in your environment.");this.logger.log(c.a.Debug,"Selecting transport '"+E[r]+"'.");try{return this.constructTransport(r)}catch(e){return e}},e.prototype.isITransport=function(e){return e&&"object"==typeof e&&"connect"in e},e.prototype.stopConnection=function(e){if(this.logger.log(c.a.Debug,"HttpConnection.stopConnection("+e+") called while in state "+this.connectionState+"."),this.transport=void 0,e=this.stopError||e,this.stopError=void 0,"Disconnected"!==this.connectionState)if("Connecting "!==this.connectionState){if("Disconnecting"===this.connectionState&&this.stopPromiseResolver(),e?this.logger.log(c.a.Error,"Connection disconnected with error '"+e+"'."):this.logger.log(c.a.Information,"Connection disconnected."),this.connectionId=void 0,this.connectionState="Disconnected",this.onclose&&this.connectionStarted){this.connectionStarted=!1;try{this.onclose(e)}catch(t){this.logger.log(c.a.Error,"HttpConnection.onclose("+e+") threw error '"+t+"'.")}}}else this.logger.log(c.a.Warning,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection hasn't yet left the in the connecting state.");else this.logger.log(c.a.Debug,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection is already in the disconnected state.")},e.prototype.resolveUrl=function(e){if(0===e.lastIndexOf("https://",0)||0===e.lastIndexOf("http://",0))return e;if(!g.c.isBrowser||!window.document)throw new Error("Cannot resolve '"+e+"'.");var t=window.document.createElement("a");return t.href=e,this.logger.log(c.a.Information,"Normalizing '"+e+"' to '"+t.href+"'."),t.href},e.prototype.resolveNegotiateUrl=function(e){var t=e.indexOf("?"),n=e.substring(0,-1===t?e.length:t);return"/"!==n[n.length-1]&&(n+="/"),n+="negotiate",-1===(n+=-1===t?"":e.substring(t)).indexOf("negotiateVersion")&&(n+=-1===t?"?":"&",n+="negotiateVersion="+this.negotiateVersion),n},e}();var q=function(){function e(e){this.transport=e,this.buffer=[],this.executing=!0,this.sendBufferedData=new W,this.transportResult=new W,this.sendLoopPromise=this.sendLoop()}return e.prototype.send=function(e){return this.bufferData(e),this.transportResult||(this.transportResult=new W),this.transportResult.promise},e.prototype.stop=function(){return this.executing=!1,this.sendBufferedData.resolve(),this.sendLoopPromise},e.prototype.bufferData=function(e){if(this.buffer.length&&typeof this.buffer[0]!=typeof e)throw new Error("Expected data to be of type "+typeof this.buffer+" but was of type "+typeof e);this.buffer.push(e),this.sendBufferedData.resolve()},e.prototype.sendLoop=function(){return B(this,void 0,void 0,function(){var t,n,r;return j(this,function(o){switch(o.label){case 0:return[4,this.sendBufferedData.promise];case 1:if(o.sent(),!this.executing)return this.transportResult&&this.transportResult.reject("Connection stopped."),[3,6];this.sendBufferedData=new W,t=this.transportResult,this.transportResult=void 0,n="string"==typeof this.buffer[0]?this.buffer.join(""):e.concatBuffers(this.buffer),this.buffer.length=0,o.label=2;case 2:return o.trys.push([2,4,,5]),[4,this.transport.send(n)];case 3:return o.sent(),t.resolve(),[3,5];case 4:return r=o.sent(),t.reject(r),[3,5];case 5:return[3,0];case 6:return[2]}})})},e.concatBuffers=function(e){for(var t=e.map(function(e){return e.byteLength}).reduce(function(e,t){return e+t}),n=new Uint8Array(t),r=0,o=0,i=e;o(); var userNameInput = appElement.FindElement(By.ClassName("user-name")).FindElement(By.TagName("input")); var acceptsTermsInput = appElement.FindElement(By.ClassName("accepts-terms")).FindElement(By.TagName("input")); - var submitButton = appElement.FindElement(By.TagName("button")); + var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]")); var messagesAccessor = CreateValidationMessagesAccessor(appElement); var submissionStatus = appElement.FindElement(By.Id("submission-status")); @@ -340,7 +340,7 @@ public void ValidationMessageDisplaysMessagesForField() var emailContainer = appElement.FindElement(By.ClassName("email")); var emailInput = emailContainer.FindElement(By.TagName("input")); var emailMessagesAccessor = CreateValidationMessagesAccessor(emailContainer); - var submitButton = appElement.FindElement(By.TagName("button")); + var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]")); // Doesn't show messages for other fields submitButton.Click(); @@ -371,7 +371,7 @@ public void ErrorsFromCompareAttribute() var confirmEmailValidationMessage = CreateValidationMessagesAccessor(confirmEmailContainer); var modelErrors = CreateValidationMessagesAccessor(appElement.FindElement(By.ClassName("model-errors"))); CreateValidationMessagesAccessor(emailContainer); - var submitButton = appElement.FindElement(By.TagName("button")); + var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]")); // Updates on edit emailInput.SendKeys("a@b.com\t"); diff --git a/src/Components/test/E2ETest/Tests/FormsTestWithExperimentalValiator.cs b/src/Components/test/E2ETest/Tests/FormsTestWithExperimentalValidator.cs similarity index 100% rename from src/Components/test/E2ETest/Tests/FormsTestWithExperimentalValiator.cs rename to src/Components/test/E2ETest/Tests/FormsTestWithExperimentalValidator.cs diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/ExperimentalValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/ExperimentalValidationComponent.razor index dd574f4dbe89..fe3df5382e9d 100644 --- a/src/Components/test/testassets/BasicTestApp/FormsTest/ExperimentalValidationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/ExperimentalValidationComponent.razor @@ -1,9 +1,13 @@ @using System.ComponentModel.DataAnnotations @using Microsoft.AspNetCore.Components.Forms +

    + This component is used to verify the use of the experimental ObjectGraphDataAnnotationsValidator type with IValidatableObject and deep validation, as well + as the ComparePropertyAttribute. +

    - +

    Name: @@ -104,7 +108,7 @@ [EmailAddress(ErrorMessage = "Enter a valid email address")] public string EmailAddress { get; set; } - [BlazorCompare(nameof(EmailAddress))] + [CompareProperty(nameof(EmailAddress))] [Display(Name = "Confirm email address")] public string ConfirmEmailAddress { get; set; } diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponent.razor index d4f8ce464fbd..2c9a4d245679 100644 --- a/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponent.razor @@ -4,7 +4,7 @@ @if (UseExperimentalValidator) { - + } else { diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor index be2abd5e7919..90b2f6a26b65 100644 --- a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor @@ -4,7 +4,7 @@ @if (UseExperimentalValidator) { - + } else { From e3ea4cb2522e0d031cf2b7e76a4d1978a7445854 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 16 Oct 2019 13:49:38 -0700 Subject: [PATCH 5/9] Fixup projectreferences.props --- eng/ProjectReferences.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 16f0ef714a24..937dce76edcf 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -137,7 +137,7 @@ - + From d780aac2cc603b28f6562617338e2ac624e0edc3 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 16 Oct 2019 14:29:45 -0700 Subject: [PATCH 6/9] sigh --- eng/ProjectReferences.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 937dce76edcf..37704e933ee5 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -137,7 +137,7 @@ - + From 648b559726b94679418384a560ddd778b9325922 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 17 Oct 2019 16:41:56 -0700 Subject: [PATCH 7/9] Fixup --- eng/ProjectReferences.props | 2 +- ...icrosoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 37704e933ee5..16f0ef714a24 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -137,7 +137,7 @@ - + diff --git a/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj b/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj index 2d73d62df1b1..a166d5f1f3d1 100644 --- a/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj +++ b/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj @@ -4,6 +4,7 @@ netstandard2.0 Provides experimental support for validation using DataAnnotations. true + false From 58e56136ca278dd2c256056dbda238bbc4d8b462 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 18 Oct 2019 05:58:31 -0700 Subject: [PATCH 8/9] Update ProjectReferences.props --- eng/ProjectReferences.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 16f0ef714a24..012b3d84b6f9 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -15,6 +15,7 @@ + @@ -137,7 +138,6 @@ - From b89535c2cf78268e3b099ab92ff5b96c2d8b6f4f Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 18 Oct 2019 08:49:33 -0700 Subject: [PATCH 9/9] Update ProjectReferences.props --- eng/ProjectReferences.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 012b3d84b6f9..3ac3e8dbf677 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -15,7 +15,7 @@ - +