Skip to content
Open
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
9 changes: 8 additions & 1 deletion src/Components/Forms/src/EditContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,17 @@ public FieldIdentifier Field(string fieldName)
/// <param name="fieldIdentifier">Identifies the field whose value has been changed.</param>
public void NotifyFieldChanged(in FieldIdentifier fieldIdentifier)
{
GetOrAddFieldState(fieldIdentifier).IsModified = true;
MarkAsModified(fieldIdentifier);
OnFieldChanged?.Invoke(this, new FieldChangedEventArgs(fieldIdentifier));
}

/// <summary>
/// Marks the specified field as modified.
/// </summary>
/// <param name="fieldIdentifier">Identifies the field whose modification flag should be set.</param>
public void MarkAsModified(in FieldIdentifier fieldIdentifier)
=> GetOrAddFieldState(fieldIdentifier).IsModified = true;

/// <summary>
/// Signals that some aspect of validation state has changed.
/// </summary>
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
Expand Up @@ -12,3 +12,4 @@ Microsoft.AspNetCore.Components.Forms.EditContext.IsValidationPending() -> bool
Microsoft.AspNetCore.Components.Forms.EditContext.IsValidationFaulted(in Microsoft.AspNetCore.Components.Forms.FieldIdentifier fieldIdentifier) -> bool
Microsoft.AspNetCore.Components.Forms.EditContext.IsValidationFaulted<TField>(System.Linq.Expressions.Expression<System.Func<TField>!>! accessor) -> bool
Microsoft.AspNetCore.Components.Forms.EditContext.IsValidationFaulted() -> bool
Microsoft.AspNetCore.Components.Forms.EditContext.MarkAsModified(in Microsoft.AspNetCore.Components.Forms.FieldIdentifier fieldIdentifier) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,28 @@ public void PerformsPerPropertyValidationOnFieldChange()
Assert.Equal(new[] { "IntFrom1To100:range" }, editContext.GetValidationMessages());
}

[Fact]
public void MarkAsModifiedDoesNotTriggerPerPropertyValidation()
{
var model = new TestModel { IntFrom1To100 = 101 };
var editContext = new EditContext(model);
editContext.EnableDataAnnotationsValidation(_serviceProvider);
var onValidationStateChangedCount = 0;
var requiredStringIdentifier = new FieldIdentifier(model, nameof(TestModel.RequiredString));
editContext.OnValidationStateChanged += (sender, eventArgs) => onValidationStateChangedCount++;

editContext.MarkAsModified(requiredStringIdentifier);

Assert.True(editContext.IsModified(requiredStringIdentifier));
Assert.Equal(0, onValidationStateChangedCount);
Assert.Empty(editContext.GetValidationMessages(requiredStringIdentifier));
Assert.Empty(editContext.GetValidationMessages());

editContext.NotifyFieldChanged(requiredStringIdentifier);
Assert.Equal(1, onValidationStateChangedCount);
Assert.Equal(new[] { "RequiredString:required" }, editContext.GetValidationMessages());
}

[Theory]
[InlineData(nameof(TestModel.ThisWillNotBeValidatedBecauseItIsAField))]
[InlineData(nameof(TestModel.ThisWillNotBeValidatedBecauseItIsInternal))]
Expand Down
15 changes: 15 additions & 0 deletions src/Components/Forms/test/EditContextTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ public void RaisesEventWhenFieldIsChanged()
Assert.True(didReceiveNotification);
}

[Fact]
public void MarkAsModifiedTracksFieldAsModifiedWithoutRaisingOnFieldChanged()
{
var editContext = new EditContext(new object());
var field1 = editContext.Field("field1");
var didReceiveNotification = false;
editContext.OnFieldChanged += (sender, eventArgs) => didReceiveNotification = true;

editContext.MarkAsModified(field1);

Assert.True(editContext.IsModified());
Assert.True(editContext.IsModified(field1));
Assert.False(didReceiveNotification);
}

[Fact]
public void CanEnumerateValidationMessagesAcrossAllStoresForSingleField()
{
Expand Down
5 changes: 3 additions & 2 deletions src/Components/Web/src/Forms/InputBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ protected string? CurrentValueAsString
_parsingValidationMessages ??= new ValidationMessageStore(EditContext);
_parsingValidationMessages.Add(FieldIdentifier, validationErrorMessage);

// Since we're not writing to CurrentValue, we'll need to notify about modification from here
EditContext.NotifyFieldChanged(FieldIdentifier);
// The raw input changed, but the model value did not. Track the user interaction
// without revalidating the unchanged model value.
EditContext.MarkAsModified(FieldIdentifier);
}
}

Expand Down
93 changes: 91 additions & 2 deletions src/Components/Web/test/Forms/InputBaseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -326,15 +326,104 @@ public async Task ParsesCurrentValueAsStringWhenChanged_Invalid()
Assert.Equal(new[] { "Bad date value" }, rootComponent.EditContext.GetValidationMessages(fieldIdentifier));
Assert.Equal(1, numValidationStateChanges);

// Act/Assert 2: Transition to valid
await inputComponent.SetCurrentValueAsStringAsync("invalid");
Assert.Empty(valueChangedArgs);
Assert.True(rootComponent.EditContext.IsModified(fieldIdentifier));
Assert.Equal(new[] { "Bad date value" }, rootComponent.EditContext.GetValidationMessages(fieldIdentifier));
Assert.Equal(2, numValidationStateChanges);

await inputComponent.SetCurrentValueAsStringAsync("1991/11/20");
var receivedParsedValue = valueChangedArgs.Single();
Assert.Equal(1991, receivedParsedValue.Year);
Assert.Equal(11, receivedParsedValue.Month);
Assert.Equal(20, receivedParsedValue.Day);
Assert.True(rootComponent.EditContext.IsModified(fieldIdentifier));
Assert.Empty(rootComponent.EditContext.GetValidationMessages(fieldIdentifier));
Assert.Equal(2, numValidationStateChanges);
Assert.Equal(3, numValidationStateChanges);
}

[Fact]
public async Task ParsingFailureDoesNotNotifyFieldChanged()
{
var model = new TestModel();
var editContext = new EditContext(model);
var rootComponent = new TestInputHostComponent<DateTime, TestDateInputComponent>
{
EditContext = editContext,
ValueChanged = _ => { },
ValueExpression = () => model.DateProperty
};
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
var fieldChangedCount = 0;
editContext.OnFieldChanged += (sender, eventArgs) => fieldChangedCount++;
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);

await inputComponent.SetCurrentValueAsStringAsync("1991/11/40");

Assert.True(editContext.IsModified(fieldIdentifier));
Assert.Equal(0, fieldChangedCount);
Assert.Equal(new[] { "Bad date value" }, editContext.GetValidationMessages(fieldIdentifier));
}

[Fact]
public async Task ValidValueAfterParsingFailureNotifiesFieldChanged()
{
var model = new TestModel();
var editContext = new EditContext(model);
var rootComponent = new TestInputHostComponent<DateTime, TestDateInputComponent>
{
EditContext = editContext,
ValueChanged = value => model.DateProperty = value,
ValueExpression = () => model.DateProperty
};
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
var fieldChangedCount = 0;
editContext.OnFieldChanged += (sender, eventArgs) => fieldChangedCount++;
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
await inputComponent.SetCurrentValueAsStringAsync("1991/11/40");
Assert.Equal(0, fieldChangedCount);

await inputComponent.SetCurrentValueAsStringAsync("1991/11/20");

Assert.Equal(new DateTime(1991, 11, 20), model.DateProperty);
Assert.True(editContext.IsModified(fieldIdentifier));
Assert.Equal(1, fieldChangedCount);
Assert.Empty(editContext.GetValidationMessages(fieldIdentifier));
}

[Fact]
public async Task ParsingFailureAfterValidValueDoesNotNotifyFieldChanged()
{
var model = new TestModel();
var editContext = new EditContext(model);
var rootComponent = new TestInputHostComponent<DateTime, TestDateInputComponent>
{
EditContext = editContext,
ValueChanged = value => model.DateProperty = value,
ValueExpression = () => model.DateProperty
};
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
var unrelatedFieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
var unrelatedMessages = new ValidationMessageStore(editContext);
var fieldChangedCount = 0;
editContext.OnFieldChanged += (sender, eventArgs) =>
{
fieldChangedCount++;
unrelatedMessages.Add(unrelatedFieldIdentifier, "Unrelated validation ran");
};
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
await inputComponent.SetCurrentValueAsStringAsync("1991/11/20");
unrelatedMessages.Clear();
editContext.MarkAsUnmodified(fieldIdentifier);

await inputComponent.SetCurrentValueAsStringAsync("1991/11/40");

Assert.Equal(new DateTime(1991, 11, 20), model.DateProperty);
Assert.True(editContext.IsModified(fieldIdentifier));
Assert.Equal(1, fieldChangedCount);
Assert.Equal(new[] { "Bad date value" }, editContext.GetValidationMessages(fieldIdentifier));
Assert.Empty(editContext.GetValidationMessages(unrelatedFieldIdentifier));
Assert.False(editContext.IsModified(unrelatedFieldIdentifier));
}

[Fact]
Expand Down
37 changes: 37 additions & 0 deletions src/Components/test/E2ETest/Tests/FormsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,43 @@ public void InputNumberInteractsWithEditContext_NullableFloat()
Browser.Empty(messagesAccessor);
}

[Fact]
public void InputNumberParsingFailureDoesNotTriggerModelValidation()
{
var appElement = MountTypicalValidationComponent();
var input = appElement.FindElement(By.ClassName("required-number")).FindElement(By.TagName("input"));
var value = appElement.FindElement(By.ClassName("required-number-value"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);

input.SendKeys("1e+10\t");

Browser.Equal("modified invalid", () => input.GetDomAttribute("class"));
Browser.Equal("", () => value.Text);
Browser.Equal(new[] { "The RequiredNumber field must be a number." }, messagesAccessor);

input.Clear();
input.SendKeys("5\t");

Browser.Equal("modified valid", () => input.GetDomAttribute("class"));
Browser.Equal("5", () => value.Text);
Browser.Empty(messagesAccessor);

input.SendKeys(Keys.Control + "a");
input.SendKeys(Keys.Delete);
input.SendKeys("1e+10\t");

Browser.Equal("modified invalid", () => input.GetDomAttribute("class"));
Browser.Equal("5", () => value.Text);
Browser.Equal(new[] { "The RequiredNumber field must be a number." }, messagesAccessor);

input.Clear();
input.SendKeys("\t");

Browser.Equal("modified invalid", () => input.GetDomAttribute("class"));
Browser.Equal("", () => value.Text);
Browser.Equal(new[] { "Enter a required number" }, messagesAccessor);
}

[Fact]
public void InputTextAreaInteractsWithEditContext()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
<p class="height">
Height (optional): <InputNumber @bind-Value="person.OptionalHeight" />
</p>
<p class="required-number">
Required number: <InputNumber @bind-Value="person.RequiredNumber" />
<span class="required-number-value">@person.RequiredNumber</span>
</p>
<p class="description">
Description: <InputTextArea @bind-Value="person.Description" placeholder="Tell us about yourself" />
</p>
Expand Down Expand Up @@ -185,6 +189,9 @@

public float? OptionalHeight { get; set; }

[Required(ErrorMessage = "Enter a required number")]
public int? RequiredNumber { get; set; }

public DateTime RenewalDate { get; set; } = DateTime.Now;

public DateTimeOffset? OptionalExpiryDate { get; set; }
Expand Down
Loading