diff --git a/src/Components/Forms/src/DataAnnotationsValidator.cs b/src/Components/Forms/src/DataAnnotationsValidator.cs index 0b52530d2271..21a67723e753 100644 --- a/src/Components/Forms/src/DataAnnotationsValidator.cs +++ b/src/Components/Forms/src/DataAnnotationsValidator.cs @@ -13,6 +13,8 @@ public class DataAnnotationsValidator : ComponentBase, IDisposable [CascadingParameter] EditContext? CurrentEditContext { get; set; } + [Inject] private IServiceProvider ServiceProvider { get; set; } = default!; + /// protected override void OnInitialized() { @@ -23,7 +25,7 @@ protected override void OnInitialized() $"inside an EditForm."); } - _subscriptions = CurrentEditContext.EnableDataAnnotationsValidation(); + _subscriptions = CurrentEditContext.EnableDataAnnotationsValidation(ServiceProvider); _originalEditContext = CurrentEditContext; } diff --git a/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs b/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs index 9574a555f008..30937f5bb248 100644 --- a/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs +++ b/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs @@ -33,10 +33,26 @@ public static EditContext AddDataAnnotationsValidation(this EditContext editCont /// /// The . /// A disposable object whose disposal will remove DataAnnotations validation support from the . + [Obsolete("This API is obsolete and may be removed in future versions.")] public static IDisposable EnableDataAnnotationsValidation(this EditContext editContext) { - return new DataAnnotationsEventSubscriptions(editContext); + return new DataAnnotationsEventSubscriptions(editContext, null!); } + /// + /// Enables DataAnnotations validation support for the . + /// + /// The . + /// The to be used in the . + /// A disposable object whose disposal will remove DataAnnotations validation support from the . + public static IDisposable EnableDataAnnotationsValidation(this EditContext editContext, IServiceProvider serviceProvider) + { + if (serviceProvider == null) + { + throw new ArgumentNullException(nameof(serviceProvider)); + } + return new DataAnnotationsEventSubscriptions(editContext, serviceProvider); + } + private static event Action? OnClearCache; @@ -50,11 +66,13 @@ private sealed class DataAnnotationsEventSubscriptions : IDisposable private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache = new(); private readonly EditContext _editContext; + private readonly IServiceProvider? _serviceProvider; private readonly ValidationMessageStore _messages; - public DataAnnotationsEventSubscriptions(EditContext editContext) + public DataAnnotationsEventSubscriptions(EditContext editContext, IServiceProvider serviceProvider) { _editContext = editContext ?? throw new ArgumentNullException(nameof(editContext)); + _serviceProvider = serviceProvider; _messages = new ValidationMessageStore(_editContext); _editContext.OnFieldChanged += OnFieldChanged; @@ -72,7 +90,7 @@ private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs) if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo)) { var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model); - var validationContext = new ValidationContext(fieldIdentifier.Model) + var validationContext = new ValidationContext(fieldIdentifier.Model, _serviceProvider, items: null) { MemberName = propertyInfo.Name }; @@ -93,7 +111,7 @@ private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs) private void OnValidationRequested(object? sender, ValidationRequestedEventArgs e) { - var validationContext = new ValidationContext(_editContext.Model); + var validationContext = new ValidationContext(_editContext.Model, _serviceProvider, items: null); var validationResults = new List(); Validator.TryValidateObject(_editContext.Model, validationContext, validationResults, true); diff --git a/src/Components/Forms/src/PublicAPI.Unshipped.txt b/src/Components/Forms/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..4f93873599a9 100644 --- a/src/Components/Forms/src/PublicAPI.Unshipped.txt +++ b/src/Components/Forms/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +static Microsoft.AspNetCore.Components.Forms.EditContextDataAnnotationsExtensions.EnableDataAnnotationsValidation(this Microsoft.AspNetCore.Components.Forms.EditContext! editContext, System.IServiceProvider! serviceProvider) -> System.IDisposable! diff --git a/src/Components/Forms/test/EditContextDataAnnotationsExtensionsTest.cs b/src/Components/Forms/test/EditContextDataAnnotationsExtensionsTest.cs index cb264bba9b74..3ebd566d4f65 100644 --- a/src/Components/Forms/test/EditContextDataAnnotationsExtensionsTest.cs +++ b/src/Components/Forms/test/EditContextDataAnnotationsExtensionsTest.cs @@ -2,16 +2,19 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Components.Test.Helpers; namespace Microsoft.AspNetCore.Components.Forms; public class EditContextDataAnnotationsExtensionsTest { + private static readonly IServiceProvider _serviceProvider = new TestServiceProvider(); + [Fact] public void CannotUseNullEditContext() { var editContext = (EditContext)null; - var ex = Assert.Throws(() => editContext.EnableDataAnnotationsValidation()); + var ex = Assert.Throws(() => editContext.EnableDataAnnotationsValidation(_serviceProvider)); Assert.Equal("editContext", ex.ParamName); } @@ -31,7 +34,7 @@ public void GetsValidationMessagesFromDataAnnotations() // Arrange var model = new TestModel { IntFrom1To100 = 101 }; var editContext = new EditContext(model); - editContext.EnableDataAnnotationsValidation(); + editContext.EnableDataAnnotationsValidation(_serviceProvider); // Act var isValid = editContext.Validate(); @@ -61,7 +64,7 @@ public void ClearsExistingValidationMessagesOnFurtherRuns() // Arrange var model = new TestModel { IntFrom1To100 = 101 }; var editContext = new EditContext(model); - editContext.EnableDataAnnotationsValidation(); + editContext.EnableDataAnnotationsValidation(_serviceProvider); // Act/Assert 1: Initially invalid Assert.False(editContext.Validate()); @@ -78,7 +81,7 @@ public void NotifiesValidationStateChangedAfterObjectValidation() // Arrange var model = new TestModel { IntFrom1To100 = 101 }; var editContext = new EditContext(model); - editContext.EnableDataAnnotationsValidation(); + editContext.EnableDataAnnotationsValidation(_serviceProvider); var onValidationStateChangedCount = 0; editContext.OnValidationStateChanged += (sender, eventArgs) => onValidationStateChangedCount++; @@ -106,7 +109,7 @@ public void PerformsPerPropertyValidationOnFieldChange() var model = new TestModel { IntFrom1To100 = 101 }; var independentTopLevelModel = new object(); // To show we can validate things on any model, not just the top-level one var editContext = new EditContext(independentTopLevelModel); - editContext.EnableDataAnnotationsValidation(); + editContext.EnableDataAnnotationsValidation(_serviceProvider); var onValidationStateChangedCount = 0; var requiredStringIdentifier = new FieldIdentifier(model, nameof(TestModel.RequiredString)); var intFrom1To100Identifier = new FieldIdentifier(model, nameof(TestModel.IntFrom1To100)); @@ -146,7 +149,7 @@ public void IgnoresFieldChangesThatDoNotCorrespondToAValidatableProperty(string { // Arrange var editContext = new EditContext(new TestModel()); - editContext.EnableDataAnnotationsValidation(); + editContext.EnableDataAnnotationsValidation(_serviceProvider); var onValidationStateChangedCount = 0; editContext.OnValidationStateChanged += (sender, eventArgs) => onValidationStateChangedCount++; @@ -165,7 +168,7 @@ public void CanDetachFromEditContext() // Arrange var model = new TestModel { IntFrom1To100 = 101 }; var editContext = new EditContext(model); - var subscription = editContext.EnableDataAnnotationsValidation(); + var subscription = editContext.EnableDataAnnotationsValidation(_serviceProvider); // Act/Assert 1: when we're attached Assert.False(editContext.Validate()); diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs index cae4ba273915..757198693e6a 100644 --- a/src/Components/test/E2ETest/Tests/FormsTest.cs +++ b/src/Components/test/E2ETest/Tests/FormsTest.cs @@ -74,6 +74,26 @@ public async Task EditFormWorksWithDataAnnotationsValidator() Browser.Equal("OnValidSubmit", () => appElement.FindElement(By.Id("last-callback")).Text); } + [Fact] + public void EditFormWorksWithDataAnnotationsValidatorAndDI() + { + var appElement = Browser.MountTestComponent(); + var form = appElement.FindElement(By.TagName("form")); + var userNameInput = appElement.FindElement(By.ClassName("the-quiz")).FindElement(By.TagName("input")); + var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]")); + var messagesAccessor = CreateValidationMessagesAccessor(appElement); + + userNameInput.SendKeys("Bacon\t"); + submitButton.Click(); + //We can only have this errormessage when DI is working + Browser.Equal(new[] { "You should not put that in a salad!" }, messagesAccessor); + + userNameInput.Clear(); + userNameInput.SendKeys("Watermelon\t"); + submitButton.Click(); + Browser.Empty(messagesAccessor); + } + [Fact] public void InputTextInteractsWithEditContext() { diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor index 936416016ab9..f08a419ef6f6 100644 --- a/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor @@ -42,7 +42,7 @@ protected override void OnInitialized() { editContext = new EditContext(person); - editContext.EnableDataAnnotationsValidation(); + editContext.EnableDataAnnotationsValidation(new TestServiceProvider()); // Wire up INotifyPropertyChanged to the EditContext person.PropertyChanged += (sender, eventArgs) => @@ -103,4 +103,10 @@ #endregion } + + public class TestServiceProvider : IServiceProvider + { + public object GetService(Type serviceType) + => throw new NotImplementedException(); + } } diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationComponentDI.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationComponentDI.razor new file mode 100644 index 000000000000..40b5fc6c1bda --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationComponentDI.razor @@ -0,0 +1,44 @@ +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Components.Forms + + + + +

+ Name something you can put in a salad: + +

+ + +
    + @foreach (var message in context.GetValidationMessages()) + { +
  • @message
  • + } +
+ +
+ +@code { + [SaladChefValidator] + public string SaladIngredient { get; set; } + + public class SaladChefValidatorAttribute : ValidationAttribute + { + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var saladChef = validationContext.GetRequiredService(); + if (saladChef.ThingsYouCanPutInASalad.Contains(value.ToString())) + { + return ValidationResult.Success; + } + return new ValidationResult("You should not put that in a salad!"); + } + } + + // Simple class to check if DI can be used in Validation attributes + public class SaladChef + { + public string[] ThingsYouCanPutInASalad = { "Strawberries", "Pineapple", "Honeydew", "Watermelon", "Grapes" }; + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index 78aef6d94e11..db9fb10c7647 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -40,6 +40,7 @@ + diff --git a/src/Components/test/testassets/BasicTestApp/Program.cs b/src/Components/test/testassets/BasicTestApp/Program.cs index ad3424dabff4..b669190d7de0 100644 --- a/src/Components/test/testassets/BasicTestApp/Program.cs +++ b/src/Components/test/testassets/BasicTestApp/Program.cs @@ -38,6 +38,7 @@ public static async Task Main(string[] args) }); builder.Services.AddScoped(); + builder.Services.AddTransient(); builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); diff --git a/src/Components/test/testassets/TestServer/ServerStartup.cs b/src/Components/test/testassets/TestServer/ServerStartup.cs index ca6dcc0b3104..9da10dfe1a72 100644 --- a/src/Components/test/testassets/TestServer/ServerStartup.cs +++ b/src/Components/test/testassets/TestServer/ServerStartup.cs @@ -29,6 +29,7 @@ public void ConfigureServices(IServiceCollection services) javaScriptInitializer: "myJsRootComponentInitializers.testInitializer"); }); services.AddSingleton(); + services.AddTransient(); // Since tests run in parallel, we use an ephemeral key provider to avoid filesystem // contention issues.