Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Components/Forms/src/DataAnnotationsValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class DataAnnotationsValidator : ComponentBase, IDisposable

[CascadingParameter] EditContext? CurrentEditContext { get; set; }

[Inject] private IServiceProvider ServiceProvider { get; set; } = default!;
Copy link
Copy Markdown
Member

@SteveSandersonMS SteveSandersonMS Jan 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm in two minds about the nullability annotation here. I know the only way you can make it null is to use an obsolete API, but existing code may do that, and then newer code will be getting told a lie by the annotation.

Should we keep it as IServiceProvider? until the obsoleted API is actually removed? Is there prior art for how we handle this scenario elsewhere?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see that @pranavkm just suggested marking it as non-nullable, so perhaps that is an established pattern about obsoletion already. Just waiting for a comment from him to confirm.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is a private property and our implementation always expects injected properties to be available. So annotating it as non-nullable seems correct. Does that sound right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're quite right. Sorry for the confusion!


/// <inheritdoc />
protected override void OnInitialized()
{
Expand All @@ -23,7 +25,7 @@ protected override void OnInitialized()
$"inside an EditForm.");
}

_subscriptions = CurrentEditContext.EnableDataAnnotationsValidation();
_subscriptions = CurrentEditContext.EnableDataAnnotationsValidation(ServiceProvider);
_originalEditContext = CurrentEditContext;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,26 @@ public static EditContext AddDataAnnotationsValidation(this EditContext editCont
/// </summary>
/// <param name="editContext">The <see cref="EditContext"/>.</param>
/// <returns>A disposable object whose disposal will remove DataAnnotations validation support from the <see cref="EditContext"/>.</returns>
[Obsolete("This API is obsolete and may be removed in future versions.")]
public static IDisposable EnableDataAnnotationsValidation(this EditContext editContext)
Comment thread
pranavkm marked this conversation as resolved.
{
return new DataAnnotationsEventSubscriptions(editContext);
return new DataAnnotationsEventSubscriptions(editContext, null!);
}
/// <summary>
/// Enables DataAnnotations validation support for the <see cref="EditContext"/>.
/// </summary>
/// <param name="editContext">The <see cref="EditContext"/>.</param>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used in the <see cref="ValidationContext"/>.</param>
/// <returns>A disposable object whose disposal will remove DataAnnotations validation support from the <see cref="EditContext"/>.</returns>
public static IDisposable EnableDataAnnotationsValidation(this EditContext editContext, IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new ArgumentNullException(nameof(serviceProvider));
}
return new DataAnnotationsEventSubscriptions(editContext, serviceProvider);
Comment thread
MariovanZeist marked this conversation as resolved.
}


private static event Action? OnClearCache;

Expand All @@ -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;
Expand All @@ -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
};
Expand All @@ -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<ValidationResult>();
Validator.TryValidateObject(_editContext.Model, validationContext, validationResults, true);

Expand Down
1 change: 1 addition & 0 deletions src/Components/Forms/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
static Microsoft.AspNetCore.Components.Forms.EditContextDataAnnotationsExtensions.EnableDataAnnotationsValidation(this Microsoft.AspNetCore.Components.Forms.EditContext! editContext, System.IServiceProvider! serviceProvider) -> System.IDisposable!
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgumentNullException>(() => editContext.EnableDataAnnotationsValidation());
var ex = Assert.Throws<ArgumentNullException>(() => editContext.EnableDataAnnotationsValidation(_serviceProvider));
Assert.Equal("editContext", ex.ParamName);
}

Expand All @@ -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();
Expand Down Expand Up @@ -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());
Expand All @@ -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++;

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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++;

Expand All @@ -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());
Expand Down
20 changes: 20 additions & 0 deletions src/Components/test/E2ETest/Tests/FormsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValidationComponentDI>();
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);
}
Comment thread
MariovanZeist marked this conversation as resolved.

[Fact]
public void InputTextInteractsWithEditContext()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -103,4 +103,10 @@

#endregion
}

public class TestServiceProvider : IServiceProvider
{
public object GetService(Type serviceType)
=> throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms

<EditForm Model="@this" autocomplete="off">
<DataAnnotationsValidator />

<p class="the-quiz">
Name something you can put in a salad:
<input @bind="SaladIngredient" class="@context.FieldCssClass(() => SaladIngredient)" />
</p>

<button type="submit">Submit</button>
<ul class="validation-errors">
@foreach (var message in context.GetValidationMessages())
{
<li class="validation-message">@message</li>
}
</ul>

</EditForm>

@code {
[SaladChefValidator]
public string SaladIngredient { get; set; }

public class SaladChefValidatorAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var saladChef = validationContext.GetRequiredService<SaladChef>();
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" };
Comment thread
pranavkm marked this conversation as resolved.
}
}
1 change: 1 addition & 0 deletions src/Components/test/testassets/BasicTestApp/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<option value="BasicTestApp.FormsTest.SimpleValidationComponentUsingExperimentalValidator">Simple validation using experimental validator</option>
<option value="BasicTestApp.FormsTest.TypicalValidationComponent">Typical validation</option>
<option value="BasicTestApp.FormsTest.TypicalValidationComponentUsingExperimentalValidator">Typical validation using experimental validator</option>
<option value="BasicTestApp.FormsTest.ValidationComponentDI">Validation with Dependency Injection</option>
<option value="BasicTestApp.FormsTest.InputFileComponent">Input file</option>
<option value="BasicTestApp.FormsTest.InputRangeComponent">Input range</option>
<option value="BasicTestApp.FormsTest.InputsWithoutEditForm">Inputs without EditForm</option>
Expand Down
1 change: 1 addition & 0 deletions src/Components/test/testassets/BasicTestApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public static async Task Main(string[] args)
});

builder.Services.AddScoped<PreserveStateService>();
builder.Services.AddTransient<FormsTest.ValidationComponentDI.SaladChef>();

builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public void ConfigureServices(IServiceCollection services)
javaScriptInitializer: "myJsRootComponentInitializers.testInitializer");
});
services.AddSingleton<ResourceRequestLog>();
services.AddTransient<BasicTestApp.FormsTest.ValidationComponentDI.SaladChef>();

// Since tests run in parallel, we use an ephemeral key provider to avoid filesystem
// contention issues.
Expand Down