- Pick one color and one country:
-
-
- red
- japan
- green
- yemen
- blue
- latvia
- orange
+ @foreach (var airline in (Airline[])Enum.GetValues(typeof(Airline)))
+ {
+
+ @airline.ToString();
+
+ }
-
-
-
- Socks color:
-
-
- Accepts terms:
-
-
- Is evil:
-
-
- Username (optional):
-
-
-
-
-
-
-
-
-
-
-
-
-
@foreach (var entry in submissionLog) {
@entry
}
+
+
+ Pick one color and one country:
+
+
+ red
+ japan
+ green
+ yemen
+ blue
+ latvia
+ orange
+
+
+
+
+ Socks color:
+
+
+ Accepts terms:
+
+
+ Is evil:
+
+
+ Username (optional):
+
+
+
+
+
+
+
+
+
+
+
+}
+
+
@foreach (var entry in submissionLog)
+{
@entry
}
+
+
@code {
protected virtual bool UseExperimentalValidator => false;
@@ -97,6 +103,7 @@
Person person = new Person();
EditContext editContext;
ValidationMessageStore customValidationMessageStore;
+ bool showForm = true;
protected override void OnInitialized()
{
@@ -187,4 +194,15 @@
_ = InvokeAsync(editContext.NotifyValidationStateChanged);
});
}
+
+ async Task RecreateEditForm()
+ {
+ // This is to show that, if a form is removed and a new one is added,
+ // the subscriptions related to the old one are cleared.
+ // Represents the bug reported at https://github.com/dotnet/aspnetcore/issues/31027
+ showForm = false;
+ await Task.Delay(1);
+ showForm = true;
+ submissionLog.Add("Recreated form");
+ }
}
diff --git a/src/Shared/E2ETesting/BrowserFixture.cs b/src/Shared/E2ETesting/BrowserFixture.cs
index 2e3838415b7c..2d53261be8c0 100644
--- a/src/Shared/E2ETesting/BrowserFixture.cs
+++ b/src/Shared/E2ETesting/BrowserFixture.cs
@@ -143,8 +143,7 @@ private async Task DeleteBrowserUserProfileDirectoriesAsync()
// Force language to english for tests
opts.AddUserProfilePreference("intl.accept_languages", "en");
- // Comment this out if you want to watch or interact with the browser (e.g., for debugging)
- if (!Debugger.IsAttached)
+ if (!Debugger.IsAttached && !string.Equals(Environment.GetEnvironmentVariable("E2E_TEST_VISIBLE"), "true", StringComparison.OrdinalIgnoreCase))
{
opts.AddArgument("--headless");
}
From d91dd28911d3d5a99c387e4865a35e6d6640034e Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Wed, 31 Mar 2021 15:36:33 +0100
Subject: [PATCH 02/11] Implement fix
---
.../Forms/src/DataAnnotationsValidator.cs | 19 ++-
.../EditContextDataAnnotationsExtensions.cs | 151 ++++++++++--------
.../Forms/src/PublicAPI.Unshipped.txt | 2 +
...fyPropertyChangedValidationComponent.razor | 3 +-
4 files changed, 109 insertions(+), 66 deletions(-)
diff --git a/src/Components/Forms/src/DataAnnotationsValidator.cs b/src/Components/Forms/src/DataAnnotationsValidator.cs
index 65e059d178a9..e08ff2332157 100644
--- a/src/Components/Forms/src/DataAnnotationsValidator.cs
+++ b/src/Components/Forms/src/DataAnnotationsValidator.cs
@@ -8,8 +8,10 @@ namespace Microsoft.AspNetCore.Components.Forms
///
/// Adds Data Annotations validation support to an .
///
- public class DataAnnotationsValidator : ComponentBase
+ public class DataAnnotationsValidator : ComponentBase, IDisposable
{
+ private IDisposable? _subscriptions;
+
[CascadingParameter] EditContext? CurrentEditContext { get; set; }
///
@@ -22,7 +24,20 @@ protected override void OnInitialized()
$"inside an EditForm.");
}
- CurrentEditContext.AddDataAnnotationsValidation();
+ _subscriptions = CurrentEditContext.EnableDataAnnotationsValidation();
+ }
+
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+
+ void IDisposable.Dispose()
+ {
+ _subscriptions?.Dispose();
+ _subscriptions = null;
+
+ Dispose(disposing: true);
}
}
}
diff --git a/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs b/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs
index 56c226455357..55d4e94f6676 100644
--- a/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs
+++ b/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs
@@ -16,98 +16,123 @@ namespace Microsoft.AspNetCore.Components.Forms
///
public static class EditContextDataAnnotationsExtensions
{
- private static ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache
- = new ConcurrentDictionary<(Type, string), PropertyInfo?>();
-
///
/// Adds DataAnnotations validation support to the .
///
/// The .
+ [Obsolete("Use " + nameof(EnableDataAnnotationsValidation) + " instead.")]
public static EditContext AddDataAnnotationsValidation(this EditContext editContext)
{
- if (editContext == null)
- {
- throw new ArgumentNullException(nameof(editContext));
- }
-
- var messages = new ValidationMessageStore(editContext);
-
- // Perform object-level validation on request
- editContext.OnValidationRequested +=
- (sender, eventArgs) => ValidateModel((EditContext)sender!, messages);
-
- // Perform per-field validation on each field edit
- editContext.OnFieldChanged +=
- (sender, eventArgs) => ValidateField(editContext, messages, eventArgs.FieldIdentifier);
-
+ EnableDataAnnotationsValidation(editContext);
return editContext;
}
- private static void ValidateModel(EditContext editContext, ValidationMessageStore messages)
+ ///
+ /// Enables DataAnnotations validation support for the .
+ ///
+ /// The .
+ /// A disposable object whose disposal will remove DataAnnotations validation support from the .
+ public static IDisposable EnableDataAnnotationsValidation(this EditContext editContext)
+ {
+ return new DataAnnotationsEventSubscriptions(editContext);
+ }
+
+ class DataAnnotationsEventSubscriptions : IDisposable
{
- var validationContext = new ValidationContext(editContext.Model);
- var validationResults = new List();
- Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true);
+ private static ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache
+ = new ConcurrentDictionary<(Type, string), PropertyInfo?>();
+
+ private readonly EditContext _editContext;
+ private readonly ValidationMessageStore _messages;
- // Transfer results to the ValidationMessageStore
- messages.Clear();
- foreach (var validationResult in validationResults)
+ public DataAnnotationsEventSubscriptions(EditContext editContext)
{
- if (validationResult == null)
+ if (editContext == null)
{
- continue;
+ throw new ArgumentNullException(nameof(editContext));
}
- if (!validationResult.MemberNames.Any())
- {
- messages.Add(new FieldIdentifier(editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage!);
- continue;
- }
+ _editContext = editContext ?? throw new ArgumentNullException(nameof(editContext));
+ _messages = new ValidationMessageStore(_editContext);
- foreach (var memberName in validationResult.MemberNames)
+ _editContext.OnFieldChanged += OnFieldChanged;
+ _editContext.OnValidationRequested += OnValidationRequested;
+ }
+
+ private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs)
+ {
+ var fieldIdentifier = eventArgs.FieldIdentifier;
+ if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
{
- messages.Add(editContext.Field(memberName), validationResult.ErrorMessage!);
+ var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
+ var validationContext = new ValidationContext(fieldIdentifier.Model)
+ {
+ MemberName = propertyInfo.Name
+ };
+ var results = new List();
+
+ Validator.TryValidateProperty(propertyValue, validationContext, results);
+ _messages.Clear(fieldIdentifier);
+ _messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage!));
+
+ // We have to notify even if there were no messages before and are still no messages now,
+ // because the "state" that changed might be the completion of some async validation task
+ _editContext.NotifyValidationStateChanged();
}
}
- editContext.NotifyValidationStateChanged();
- }
-
- private static void ValidateField(EditContext editContext, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier)
- {
- if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
+ private void OnValidationRequested(object? sender, ValidationRequestedEventArgs e)
{
- var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
- var validationContext = new ValidationContext(fieldIdentifier.Model)
+ var validationContext = new ValidationContext(_editContext.Model);
+ var validationResults = new List();
+ Validator.TryValidateObject(_editContext.Model, validationContext, validationResults, true);
+
+ // Transfer results to the ValidationMessageStore
+ _messages.Clear();
+ foreach (var validationResult in validationResults)
{
- MemberName = propertyInfo.Name
- };
- var results = new List();
+ if (validationResult == null)
+ {
+ continue;
+ }
+
+ if (!validationResult.MemberNames.Any())
+ {
+ _messages.Add(new FieldIdentifier(_editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage!);
+ continue;
+ }
+
+ foreach (var memberName in validationResult.MemberNames)
+ {
+ _messages.Add(_editContext.Field(memberName), validationResult.ErrorMessage!);
+ }
+ }
- Validator.TryValidateProperty(propertyValue, validationContext, results);
- messages.Clear(fieldIdentifier);
- messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage!));
+ _editContext.NotifyValidationStateChanged();
+ }
- // We have to notify even if there were no messages before and are still no messages now,
- // because the "state" that changed might be the completion of some async validation task
- editContext.NotifyValidationStateChanged();
+ public void Dispose()
+ {
+ _editContext.OnFieldChanged -= OnFieldChanged;
+ _editContext.OnValidationRequested -= OnValidationRequested;
+ _messages.Clear();
}
- }
- private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo? propertyInfo)
- {
- var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
- if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo))
+ private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo? propertyInfo)
{
- // DataAnnotations only validates public properties, so that's all we'll look for
- // If we can't find it, cache 'null' so we don't have to try again next time
- propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
+ var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
+ if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo))
+ {
+ // DataAnnotations only validates public properties, so that's all we'll look for
+ // If we can't find it, cache 'null' so we don't have to try again next time
+ propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
- // No need to lock, because it doesn't matter if we write the same value twice
- _propertyInfoCache[cacheKey] = propertyInfo;
- }
+ // No need to lock, because it doesn't matter if we write the same value twice
+ _propertyInfoCache[cacheKey] = propertyInfo;
+ }
- return propertyInfo != null;
+ return propertyInfo != null;
+ }
}
}
}
diff --git a/src/Components/Forms/src/PublicAPI.Unshipped.txt b/src/Components/Forms/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..a412262cbbf0 100644
--- a/src/Components/Forms/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Forms/src/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+static Microsoft.AspNetCore.Components.Forms.EditContextDataAnnotationsExtensions.EnableDataAnnotationsValidation(this Microsoft.AspNetCore.Components.Forms.EditContext! editContext) -> System.IDisposable!
+virtual Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator.Dispose(bool disposing) -> void
diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor
index a257c1f2c97b..936416016ab9 100644
--- a/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor
@@ -41,7 +41,8 @@
protected override void OnInitialized()
{
- editContext = new EditContext(person).AddDataAnnotationsValidation();
+ editContext = new EditContext(person);
+ editContext.EnableDataAnnotationsValidation();
// Wire up INotifyPropertyChanged to the EditContext
person.PropertyChanged += (sender, eventArgs) =>
From 01c7eedcf3c5e247afed79f325bb34888c00d1b4 Mon Sep 17 00:00:00 2001
From: Steve Sanderson
Date: Wed, 31 Mar 2021 15:56:47 +0100
Subject: [PATCH 03/11] Fully update the validation state after disposal. Make
the E2E test able to see this.
---
.../EditContextDataAnnotationsExtensions.cs | 3 +-
.../test/E2ETest/Tests/FormsTest.cs | 28 ++-
.../TypicalValidationComponent.razor | 187 +++++++++---------
3 files changed, 111 insertions(+), 107 deletions(-)
diff --git a/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs b/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs
index 55d4e94f6676..451f4fd2ff9d 100644
--- a/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs
+++ b/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs
@@ -113,9 +113,10 @@ private void OnValidationRequested(object? sender, ValidationRequestedEventArgs
public void Dispose()
{
+ _messages.Clear();
_editContext.OnFieldChanged -= OnFieldChanged;
_editContext.OnValidationRequested -= OnValidationRequested;
- _messages.Clear();
+ _editContext.NotifyValidationStateChanged();
}
private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo? propertyInfo)
diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs
index e588a0c840a1..847480b55c20 100644
--- a/src/Components/test/E2ETest/Tests/FormsTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsTest.cs
@@ -573,21 +573,29 @@ public void NavigateOnSubmitWorks()
}
[Fact]
- public void EditFormSubscriptionsAreRemovedOnDisposal()
+ public void CanRemoveAndReAddDataAnnotationsSupport()
{
var appElement = MountTypicalValidationComponent();
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
+ var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
+ Func lastLogEntryAccessor = () => appElement.FindElement(By.CssSelector(".submission-log-entry:last-of-type")).Text;
- // Remove the old form and add a new one
- appElement.FindElement(By.Id("recreate-edit-form")).Click();
- Browser.Equal("Recreated form", () => appElement.FindElement(By.CssSelector(".submission-log-entry:last-of-type")).Text);
+ nameInput.SendKeys("01234567890123456789\t");
+ Browser.Equal("modified invalid", () => nameInput.GetAttribute("class"));
+ Browser.Equal(new[] { "That name is too long" }, messagesAccessor);
- // Verify there's still only one copy of each validation message
- var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
- nameInput.SendKeys("Bert\t");
- nameInput.Clear();
- nameInput.SendKeys("\t");
- Browser.Equal(new[] { "Enter a name" }, messagesAccessor);
+ // Remove DataAnnotations support
+ appElement.FindElement(By.Id("toggle-dataannotations")).Click();
+ Browser.Equal("DataAnnotations support is now disabled", lastLogEntryAccessor);
+ Browser.Equal("modified valid", () => nameInput.GetAttribute("class"));
+ Browser.Empty(messagesAccessor);
+
+ // Re-add DataAnnotations support
+ appElement.FindElement(By.Id("toggle-dataannotations")).Click();
+ nameInput.SendKeys("0\t");
+ Browser.Equal("DataAnnotations support is now enabled", lastLogEntryAccessor);
+ Browser.Equal("modified invalid", () => nameInput.GetAttribute("class"));
+ Browser.Equal(new[] { "That name is too long" }, messagesAccessor);
}
private Func CreateValidationMessagesAccessor(IWebElement appElement)
diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
index 5194dce6792e..8bab42c8c3a6 100644
--- a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
@@ -1,101 +1,101 @@
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms
-@if (showForm)
-{
-
+
+ @if (enableDataAnnotationsSupport)
+ {
+ }
-