Make InputBase respond to validation state notifications (#14818)
* InputBase subscribes to OnValidationStateChanged. Fixes #11914 * E2E test
This commit is contained in:
parent
2c6d7a0cb8
commit
d3f1f5a6ea
|
|
@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
|
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
|
||||||
protected override void OnParametersSet() { }
|
protected override void OnParametersSet() { }
|
||||||
}
|
}
|
||||||
public abstract partial class InputBase<TValue> : Microsoft.AspNetCore.Components.ComponentBase
|
public abstract partial class InputBase<TValue> : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable
|
||||||
{
|
{
|
||||||
protected InputBase() { }
|
protected InputBase() { }
|
||||||
[Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)]
|
[Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)]
|
||||||
|
|
@ -58,8 +58,10 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
public Microsoft.AspNetCore.Components.EventCallback<TValue> ValueChanged { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public Microsoft.AspNetCore.Components.EventCallback<TValue> ValueChanged { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||||
public System.Linq.Expressions.Expression<System.Func<TValue>> ValueExpression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public System.Linq.Expressions.Expression<System.Func<TValue>> ValueExpression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
|
protected virtual void Dispose(bool disposing) { }
|
||||||
protected virtual string FormatValueAsString(TValue value) { throw null; }
|
protected virtual string FormatValueAsString(TValue value) { throw null; }
|
||||||
public override System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
|
public override System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
|
||||||
|
void System.IDisposable.Dispose() { }
|
||||||
protected abstract bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage);
|
protected abstract bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage);
|
||||||
}
|
}
|
||||||
public partial class InputCheckbox : Microsoft.AspNetCore.Components.Forms.InputBase<bool>
|
public partial class InputCheckbox : Microsoft.AspNetCore.Components.Forms.InputBase<bool>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
|
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
|
||||||
protected override void OnParametersSet() { }
|
protected override void OnParametersSet() { }
|
||||||
}
|
}
|
||||||
public abstract partial class InputBase<TValue> : Microsoft.AspNetCore.Components.ComponentBase
|
public abstract partial class InputBase<TValue> : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable
|
||||||
{
|
{
|
||||||
protected InputBase() { }
|
protected InputBase() { }
|
||||||
[Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)]
|
[Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)]
|
||||||
|
|
@ -58,8 +58,10 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
public Microsoft.AspNetCore.Components.EventCallback<TValue> ValueChanged { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public Microsoft.AspNetCore.Components.EventCallback<TValue> ValueChanged { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||||
public System.Linq.Expressions.Expression<System.Func<TValue>> ValueExpression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public System.Linq.Expressions.Expression<System.Func<TValue>> ValueExpression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
|
protected virtual void Dispose(bool disposing) { }
|
||||||
protected virtual string FormatValueAsString(TValue value) { throw null; }
|
protected virtual string FormatValueAsString(TValue value) { throw null; }
|
||||||
public override System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
|
public override System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
|
||||||
|
void System.IDisposable.Dispose() { }
|
||||||
protected abstract bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage);
|
protected abstract bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage);
|
||||||
}
|
}
|
||||||
public partial class InputCheckbox : Microsoft.AspNetCore.Components.Forms.InputBase<bool>
|
public partial class InputCheckbox : Microsoft.AspNetCore.Components.Forms.InputBase<bool>
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,9 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
/// integrates with an <see cref="Forms.EditContext"/>, which must be supplied
|
/// integrates with an <see cref="Forms.EditContext"/>, which must be supplied
|
||||||
/// as a cascading parameter.
|
/// as a cascading parameter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class InputBase<TValue> : ComponentBase
|
public abstract class InputBase<TValue> : ComponentBase, IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly EventHandler<ValidationStateChangedEventArgs> _validationStateChangedHandler;
|
||||||
private bool _previousParsingAttemptFailed;
|
private bool _previousParsingAttemptFailed;
|
||||||
private ValidationMessageStore _parsingValidationMessages;
|
private ValidationMessageStore _parsingValidationMessages;
|
||||||
private Type _nullableUnderlyingType;
|
private Type _nullableUnderlyingType;
|
||||||
|
|
@ -121,6 +122,14 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs an instance of <see cref="InputBase{TValue}"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected InputBase()
|
||||||
|
{
|
||||||
|
_validationStateChangedHandler = (sender, eventArgs) => StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Formats the value as a string. Derived classes can override this to determine the formating used for <see cref="CurrentValueAsString"/>.
|
/// Formats the value as a string. Derived classes can override this to determine the formating used for <see cref="CurrentValueAsString"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -193,6 +202,8 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
EditContext = CascadedEditContext;
|
EditContext = CascadedEditContext;
|
||||||
FieldIdentifier = FieldIdentifier.Create(ValueExpression);
|
FieldIdentifier = FieldIdentifier.Create(ValueExpression);
|
||||||
_nullableUnderlyingType = Nullable.GetUnderlyingType(typeof(TValue));
|
_nullableUnderlyingType = Nullable.GetUnderlyingType(typeof(TValue));
|
||||||
|
|
||||||
|
EditContext.OnValidationStateChanged += _validationStateChangedHandler;
|
||||||
}
|
}
|
||||||
else if (CascadedEditContext != EditContext)
|
else if (CascadedEditContext != EditContext)
|
||||||
{
|
{
|
||||||
|
|
@ -208,5 +219,19 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
// For derived components, retain the usual lifecycle with OnInit/OnParametersSet/etc.
|
// For derived components, retain the usual lifecycle with OnInit/OnParametersSet/etc.
|
||||||
return base.SetParametersAsync(ParameterView.Empty);
|
return base.SetParametersAsync(ParameterView.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDisposable.Dispose()
|
||||||
|
{
|
||||||
|
if (EditContext != null)
|
||||||
|
{
|
||||||
|
EditContext.OnValidationStateChanged -= _validationStateChangedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispose(disposing: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -294,7 +294,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
|
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
inputComponent.CurrentValueAsString = "1991/11/20";
|
await inputComponent.SetCurrentValueAsStringAsync("1991/11/20");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var receivedParsedValue = valueChangedArgs.Single();
|
var receivedParsedValue = valueChangedArgs.Single();
|
||||||
|
|
@ -324,14 +324,14 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
|
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
|
||||||
|
|
||||||
// Act/Assert 1: Transition to invalid
|
// Act/Assert 1: Transition to invalid
|
||||||
inputComponent.CurrentValueAsString = "1991/11/40";
|
await inputComponent.SetCurrentValueAsStringAsync("1991/11/40");
|
||||||
Assert.Empty(valueChangedArgs);
|
Assert.Empty(valueChangedArgs);
|
||||||
Assert.True(rootComponent.EditContext.IsModified(fieldIdentifier));
|
Assert.True(rootComponent.EditContext.IsModified(fieldIdentifier));
|
||||||
Assert.Equal(new[] { "Bad date value" }, rootComponent.EditContext.GetValidationMessages(fieldIdentifier));
|
Assert.Equal(new[] { "Bad date value" }, rootComponent.EditContext.GetValidationMessages(fieldIdentifier));
|
||||||
Assert.Equal(1, numValidationStateChanges);
|
Assert.Equal(1, numValidationStateChanges);
|
||||||
|
|
||||||
// Act/Assert 2: Transition to valid
|
// Act/Assert 2: Transition to valid
|
||||||
inputComponent.CurrentValueAsString = "1991/11/20";
|
await inputComponent.SetCurrentValueAsStringAsync("1991/11/20");
|
||||||
var receivedParsedValue = valueChangedArgs.Single();
|
var receivedParsedValue = valueChangedArgs.Single();
|
||||||
Assert.Equal(1991, receivedParsedValue.Year);
|
Assert.Equal(1991, receivedParsedValue.Year);
|
||||||
Assert.Equal(11, receivedParsedValue.Month);
|
Assert.Equal(11, receivedParsedValue.Month);
|
||||||
|
|
@ -341,6 +341,65 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
Assert.Equal(2, numValidationStateChanges);
|
Assert.Equal(2, numValidationStateChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RespondsToValidationStateChangeNotifications()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new TestModel();
|
||||||
|
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
|
||||||
|
{
|
||||||
|
EditContext = new EditContext(model),
|
||||||
|
ValueExpression = () => model.StringProperty
|
||||||
|
};
|
||||||
|
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
|
||||||
|
var renderer = new TestRenderer();
|
||||||
|
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
|
||||||
|
await renderer.RenderRootComponentAsync(rootComponentId);
|
||||||
|
|
||||||
|
// Initally, it rendered one batch and is valid
|
||||||
|
var batch1 = renderer.Batches.Single();
|
||||||
|
var componentFrame1 = batch1.GetComponentFrames<TestInputComponent<string>>().Single();
|
||||||
|
var inputComponentId = componentFrame1.ComponentId;
|
||||||
|
var component = (TestInputComponent<string>)componentFrame1.Component;
|
||||||
|
Assert.Equal("valid", component.CssClass);
|
||||||
|
|
||||||
|
// Act: update the field state in the EditContext and notify
|
||||||
|
var messageStore = new ValidationMessageStore(rootComponent.EditContext);
|
||||||
|
messageStore.Add(fieldIdentifier, "Some message");
|
||||||
|
await renderer.Dispatcher.InvokeAsync(rootComponent.EditContext.NotifyValidationStateChanged);
|
||||||
|
|
||||||
|
// Assert: The input component rendered itself again and now has the new class
|
||||||
|
var batch2 = renderer.Batches.Skip(1).Single();
|
||||||
|
Assert.Equal(inputComponentId, batch2.DiffsByComponentId.Keys.Single());
|
||||||
|
Assert.Equal("invalid", component.CssClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UnsubscribesFromValidationStateChangeNotifications()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new TestModel();
|
||||||
|
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>>
|
||||||
|
{
|
||||||
|
EditContext = new EditContext(model),
|
||||||
|
ValueExpression = () => model.StringProperty
|
||||||
|
};
|
||||||
|
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
|
||||||
|
var renderer = new TestRenderer();
|
||||||
|
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
|
||||||
|
await renderer.RenderRootComponentAsync(rootComponentId);
|
||||||
|
var component = renderer.Batches.Single().GetComponentFrames<TestInputComponent<string>>().Single().Component;
|
||||||
|
|
||||||
|
// Act: dispose, then update the field state in the EditContext and notify
|
||||||
|
((IDisposable)component).Dispose();
|
||||||
|
var messageStore = new ValidationMessageStore(rootComponent.EditContext);
|
||||||
|
messageStore.Add(fieldIdentifier, "Some message");
|
||||||
|
await renderer.Dispatcher.InvokeAsync(rootComponent.EditContext.NotifyValidationStateChanged);
|
||||||
|
|
||||||
|
// Assert: No additional render
|
||||||
|
Assert.Empty(renderer.Batches.Skip(1));
|
||||||
|
}
|
||||||
|
|
||||||
private static TComponent FindComponent<TComponent>(CapturedBatch batch)
|
private static TComponent FindComponent<TComponent>(CapturedBatch batch)
|
||||||
=> batch.ReferenceFrames
|
=> batch.ReferenceFrames
|
||||||
.Where(f => f.FrameType == RenderTreeFrameType.Component)
|
.Where(f => f.FrameType == RenderTreeFrameType.Component)
|
||||||
|
|
@ -376,7 +435,6 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
public new string CurrentValueAsString
|
public new string CurrentValueAsString
|
||||||
{
|
{
|
||||||
get => base.CurrentValueAsString;
|
get => base.CurrentValueAsString;
|
||||||
set { base.CurrentValueAsString = value; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public new IReadOnlyDictionary<string, object> AdditionalAttributes => base.AdditionalAttributes;
|
public new IReadOnlyDictionary<string, object> AdditionalAttributes => base.AdditionalAttributes;
|
||||||
|
|
@ -391,6 +449,15 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetCurrentValueAsStringAsync(string value)
|
||||||
|
{
|
||||||
|
// This is equivalent to the subclass writing to CurrentValueAsString
|
||||||
|
// (e.g., from @bind), except to simplify the test code there's an InvokeAsync
|
||||||
|
// here. In production code it wouldn't normally be required because @bind
|
||||||
|
// calls run on the sync context anyway.
|
||||||
|
await InvokeAsync(() => { base.CurrentValueAsString = value; });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestDateInputComponent : TestInputComponent<DateTime>
|
class TestDateInputComponent : TestInputComponent<DateTime>
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,24 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
Browser.Equal("Premium", () => selectedTicketClassDisplay.Text);
|
Browser.Equal("Premium", () => selectedTicketClassDisplay.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InputComponentsRespondToAsynchronouslyAddedMessages()
|
||||||
|
{
|
||||||
|
var appElement = Browser.MountTestComponent<TypicalValidationComponent>();
|
||||||
|
var input = appElement.FindElement(By.CssSelector(".username input"));
|
||||||
|
var triggerAsyncErrorButton = appElement.FindElement(By.CssSelector(".username button"));
|
||||||
|
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
|
||||||
|
|
||||||
|
// Initially shows no error
|
||||||
|
Browser.Empty(() => messagesAccessor());
|
||||||
|
Browser.Equal("valid", () => input.GetAttribute("class"));
|
||||||
|
|
||||||
|
// Can trigger async error
|
||||||
|
triggerAsyncErrorButton.Click();
|
||||||
|
Browser.Equal(new[] { "This is invalid, asynchronously" }, messagesAccessor);
|
||||||
|
Browser.Equal("invalid", () => input.GetAttribute("class"));
|
||||||
|
}
|
||||||
|
|
||||||
private Func<string[]> CreateValidationMessagesAccessor(IWebElement appElement)
|
private Func<string[]> CreateValidationMessagesAccessor(IWebElement appElement)
|
||||||
{
|
{
|
||||||
return () => appElement.FindElements(By.ClassName("validation-message"))
|
return () => appElement.FindElements(By.ClassName("validation-message"))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
@using System.ComponentModel.DataAnnotations
|
@using System.ComponentModel.DataAnnotations
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
|
||||||
<EditForm Model="@person" OnValidSubmit="@HandleValidSubmit">
|
<EditForm EditContext="@editContext" OnValidSubmit="@HandleValidSubmit">
|
||||||
<DataAnnotationsValidator />
|
<DataAnnotationsValidator />
|
||||||
|
|
||||||
<p class="name">
|
<p class="name">
|
||||||
|
|
@ -42,6 +42,10 @@
|
||||||
<p class="is-evil">
|
<p class="is-evil">
|
||||||
Is evil: <InputCheckbox @bind-Value="person.IsEvil" />
|
Is evil: <InputCheckbox @bind-Value="person.IsEvil" />
|
||||||
</p>
|
</p>
|
||||||
|
<p class="username">
|
||||||
|
Username (optional): <InputText @bind-Value="person.Username" />
|
||||||
|
<button type="button" @onclick="@TriggerAsyncValidationError">Trigger async error</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
|
|
||||||
|
|
@ -52,6 +56,14 @@
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
Person person = new Person();
|
Person person = new Person();
|
||||||
|
EditContext editContext;
|
||||||
|
ValidationMessageStore customValidationMessageStore;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
editContext = new EditContext(person);
|
||||||
|
customValidationMessageStore = new ValidationMessageStore(editContext);
|
||||||
|
}
|
||||||
|
|
||||||
// Usually this would be in a different file
|
// Usually this would be in a different file
|
||||||
class Person
|
class Person
|
||||||
|
|
@ -83,6 +95,8 @@
|
||||||
|
|
||||||
[Required, EnumDataType(typeof(TicketClass))]
|
[Required, EnumDataType(typeof(TicketClass))]
|
||||||
public TicketClass TicketClass { get; set; }
|
public TicketClass TicketClass { get; set; }
|
||||||
|
|
||||||
|
public string Username { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TicketClass { Economy, Premium, First }
|
enum TicketClass { Economy, Premium, First }
|
||||||
|
|
@ -93,4 +107,22 @@
|
||||||
{
|
{
|
||||||
submissionLog.Add("OnValidSubmit");
|
submissionLog.Add("OnValidSubmit");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TriggerAsyncValidationError()
|
||||||
|
{
|
||||||
|
customValidationMessageStore.Clear();
|
||||||
|
|
||||||
|
// Note that this method returns void, so the renderer doesn't react to
|
||||||
|
// its async flow by default. This is to simulate some external system
|
||||||
|
// implementing async validation.
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
// The duration of the delay doesn't matter to the test, as long as it's not
|
||||||
|
// so long that we time out. Picking a value that's long enough for humans
|
||||||
|
// to observe the asynchrony too.
|
||||||
|
await Task.Delay(500);
|
||||||
|
customValidationMessageStore.Add(editContext.Field(nameof(Person.Username)), "This is invalid, asynchronously");
|
||||||
|
_ = InvokeAsync(editContext.NotifyValidationStateChanged);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue