From ffdad0ad5330e31fe21c2a10596224294537e501 Mon Sep 17 00:00:00 2001
From: NanthiniMahalingam
<105482474+NanthiniMahalingam@users.noreply.github.com>
Date: Wed, 17 Jun 2026 16:55:36 +0530
Subject: [PATCH 1/8] Added the fix for date time value resetting to null.
---
.../Components/src/BindConverter.cs | 26 +++++++++++++------
.../EventCallbackFactoryBinderExtensions.cs | 14 ++++++++--
2 files changed, 30 insertions(+), 10 deletions(-)
diff --git a/src/Components/Components/src/BindConverter.cs b/src/Components/Components/src/BindConverter.cs
index a70d06d0d1d7..5506df0a40e5 100644
--- a/src/Components/Components/src/BindConverter.cs
+++ b/src/Components/Components/src/BindConverter.cs
@@ -380,7 +380,11 @@ private static string FormatDateTimeValueCore(DateTime value, CultureInfo? cultu
{
if (value == null)
{
- return null;
+ // Return an empty string (rather than null) so a null nullable date formats the same way an
+ // empty date input reports its value to the server. This keeps the rendered 'value' attribute
+ // in sync with what the browser already shows, preventing the diff from re-writing it and
+ // resetting the caret/segments while the user is typing (e.g. ).
+ return string.Empty;
}
if (format != null)
@@ -395,7 +399,7 @@ private static string FormatDateTimeValueCore(DateTime value, CultureInfo? cultu
{
if (value == null)
{
- return null;
+ return string.Empty;
}
return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
@@ -466,7 +470,9 @@ private static string FormatDateTimeOffsetValueCore(DateTimeOffset value, Cultur
{
if (value == null)
{
- return null;
+ // See FormatNullableDateTimeValueCore: a null value formats as empty so the rendered
+ // 'value' attribute matches what an empty date/time input reports to the server.
+ return string.Empty;
}
if (format != null)
@@ -481,7 +487,7 @@ private static string FormatDateTimeOffsetValueCore(DateTimeOffset value, Cultur
{
if (value == null)
{
- return null;
+ return string.Empty;
}
return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
@@ -553,7 +559,9 @@ private static string FormatDateOnlyValueCore(DateOnly value, CultureInfo? cultu
{
if (value == null)
{
- return null;
+ // See FormatNullableDateTimeValueCore: a null value formats as empty so the rendered
+ // 'value' attribute matches what an empty date/time input reports to the server.
+ return string.Empty;
}
if (format != null)
@@ -569,7 +577,7 @@ private static string FormatDateOnlyValueCore(DateOnly value, CultureInfo? cultu
{
if (value == null)
{
- return null;
+ return string.Empty;
}
return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
@@ -641,7 +649,9 @@ private static string FormatTimeOnlyValueCore(TimeOnly value, CultureInfo? cultu
{
if (value == null)
{
- return null;
+ // See FormatNullableDateTimeValueCore: a null value formats as empty so the rendered
+ // 'value' attribute matches what an empty date/time input reports to the server.
+ return string.Empty;
}
if (format != null)
@@ -657,7 +667,7 @@ private static string FormatTimeOnlyValueCore(TimeOnly value, CultureInfo? cultu
{
if (value == null)
{
- return null;
+ return string.Empty;
}
return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
diff --git a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs
index 1532ff08b326..2d2e1e4497ec 100644
--- a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs
+++ b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs
@@ -1373,7 +1373,12 @@ private static EventCallback CreateBinderCore(
}
else if (string.Empty.Equals(e.Value))
{
- setter(default!);
+ var typeInfo = typeof(T);
+ var isNullable = typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>);
+ if (typeInfo == typeof(string) || isNullable)
+ {
+ setter(default!);
+ }
}
};
return factory.Create(receiver, callback);
@@ -1460,7 +1465,12 @@ private static EventCallback CreateBinderCore(
}
else if (string.Empty.Equals(e.Value))
{
- setter(default!);
+ var typeInfo = typeof(T);
+ var isNullable = typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>);
+ if (typeInfo == typeof(string) || isNullable)
+ {
+ setter(default!);
+ }
}
};
return factory.Create(receiver, callback);
From 6a413ff3b521d1f3b0b954d558f61c070faaef9a Mon Sep 17 00:00:00 2001
From: NanthiniMahalingam
<105482474+NanthiniMahalingam@users.noreply.github.com>
Date: Thu, 18 Jun 2026 15:56:09 +0530
Subject: [PATCH 2/8] Added the test cases.
---
...ventCallbackFactoryBinderExtensionsTest.cs | 41 +++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs b/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs
index f23af117c53e..80efbd8ba62a 100644
--- a/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs
+++ b/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs
@@ -458,6 +458,47 @@ public async Task CreateBinder_NullableDateTime_Format()
Assert.Equal(1, component.Count);
}
+ [Fact]
+ public async Task CreateBinder_NonNullableDateTime_EmptyValue_DoesNotResetBoundValue()
+ {
+ // Regression test for https://github.com/dotnet/aspnetcore/issues/40660
+ // While the user edits a bound to a non-nullable
+ // DateTime, the browser briefly reports an empty value for the change event. The
+ // binder must NOT reset the bound field to default(DateTime) (0001-01-01) in that
+ // case, otherwise the component re-renders the default date and the partially
+ // entered value is lost/reset.
+ var value = new DateTime(2022, 2, 10);
+ var component = new EventCountingComponent();
+ Action setter = (_) => value = _;
+
+ var binder = EventCallback.Factory.CreateBinder(component, setter, value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
+
+ await binder.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });
+
+ Assert.Equal(new DateTime(2022, 2, 10), value);
+ Assert.Equal(1, component.Count);
+ }
+
+ [Fact]
+ public async Task CreateBinder_NonNullableDateTime_EmptyValue_PreservesEachBoundValue()
+ {
+ var value = new DateTime(2022, 2, 10);
+ var component = new EventCountingComponent();
+ Action setter = (_) => value = _;
+
+ EventCallback binder = EventCallback.Factory.CreateBinder(component, setter, value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
+ await binder.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });
+ Assert.Equal(1, component.Count);
+ var value1 = new DateTime(2023, 02, 09);
+ Action setter1 = (_) => value1 = _;
+
+ var binder1 = EventCallback.Factory.CreateBinder(component, setter1, value1, "yyyy-MM-dd", CultureInfo.InvariantCulture);
+ await binder1.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });
+ // The setter must not have been called, so the previous valid value is preserved.
+ Assert.Equal(new DateTime(2023, 02, 9), value1);
+ Assert.Equal(2, component.Count);
+ }
+
[Fact]
public async Task CreateBinder_DateTimeOffset()
{
From 03f2bb7a03e96810fc482e78766fa2ab3c7ec56d Mon Sep 17 00:00:00 2001
From: NanthiniMahalingam
<105482474+NanthiniMahalingam@users.noreply.github.com>
Date: Mon, 29 Jun 2026 11:20:09 +0530
Subject: [PATCH 3/8] Resolved the flaky failed test case.
---
.../BlazorWasmTestAppFixture.cs | 11 +++++++++--
.../test/E2ETest/Tests/FormsInputDateTest.cs | 19 +++++++++++++++----
2 files changed, 24 insertions(+), 6 deletions(-)
diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BlazorWasmTestAppFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BlazorWasmTestAppFixture.cs
index 778e544fd516..fcfddb631b7c 100644
--- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BlazorWasmTestAppFixture.cs
+++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BlazorWasmTestAppFixture.cs
@@ -50,7 +50,7 @@ protected override IHost CreateWebHost()
{
"--urls", $"http://{host}:0",
"--contentroot", ContentRoot,
- "--pathbase", PathBase,
+ "--Gateway:PathBase", PathBase,
"--staticWebAssets", Path.ChangeExtension(assemblyLocation, ".staticwebassets.runtime.json"),
"--ClientApps:app:EndpointsManifest", Path.ChangeExtension(assemblyLocation, ".staticwebassets.endpoints.json"),
"--ClientApps:app:PathPrefix", "",
@@ -62,7 +62,14 @@ protected override IHost CreateWebHost()
args.Add(Environment);
}
- return BlazorGateway.BuildWebHost(args.ToArray());
+ var app = BlazorGateway.BuildWebHost(args.ToArray());
+
+ // BlazorGateway serves individual static assets (e.g. index.html, _framework/*) but does not
+ // register a default-document/SPA fallback. The E2E tests navigate to the app's path base
+ // (e.g. /subdir), so fall back to index.html for requests that don't match a static asset.
+ app.MapFallbackToFile("index.html");
+
+ return app;
}
private IHost CreateStaticWebHost(string contentRoot)
diff --git a/src/Components/test/E2ETest/Tests/FormsInputDateTest.cs b/src/Components/test/E2ETest/Tests/FormsInputDateTest.cs
index 1308d87b9ed9..9837872592ab 100644
--- a/src/Components/test/E2ETest/Tests/FormsInputDateTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsInputDateTest.cs
@@ -74,16 +74,16 @@ public void InputDateInteractsWithEditContext_NullableDateTimeOffset()
// Validates on edit
Browser.Equal("valid", () => expiryDateInput.GetDomAttribute("class"));
- expiryDateInput.SendKeys("01-01-2000\t");
+ SetDateInputValue(expiryDateInput, "2000-01-01");
Browser.Equal("modified valid", () => expiryDateInput.GetDomAttribute("class"));
- // Can become invalid
- expiryDateInput.SendKeys("11-11-11111\t");
+ // Can become invalid (year is out of range for DateTimeOffset)
+ SetDateInputValue(expiryDateInput, "11111-11-11");
Browser.Equal("modified invalid", () => expiryDateInput.GetDomAttribute("class"));
Browser.Equal(new[] { "The OptionalExpiryDate field must be a date." }, messagesAccessor);
// Empty is valid, because it's nullable
- expiryDateInput.SendKeys($"{Keys.Backspace}\t{Keys.Backspace}\t{Keys.Backspace}\t");
+ SetDateInputValue(expiryDateInput, "");
Browser.Equal("modified valid", () => expiryDateInput.GetDomAttribute("class"));
Browser.Empty(messagesAccessor);
}
@@ -241,4 +241,15 @@ private Func CreateValidationMessagesAccessor(IWebElement appElement)
.OrderBy(x => x)
.ToArray();
}
+
+ // Sets the value of a native date/time input directly and raises the "change" event that
+ // InputDate binds to. This avoids the well-known flakiness of driving the browser's native
+ // date-picker segments via simulated keystrokes (see the class-level comment above), while
+ // still exercising the real value-parsing and EditContext validation path.
+ private void SetDateInputValue(IWebElement input, string value)
+ => ((IJavaScriptExecutor)Browser).ExecuteScript(
+ "arguments[0].value = arguments[1];" +
+ "arguments[0].dispatchEvent(new Event('change', { bubbles: true }));",
+ input,
+ value);
}
From c03de6743a08fc181149b9ed90e9a51ac9ed59bd Mon Sep 17 00:00:00 2001
From: NanthiniMahalingam
<105482474+NanthiniMahalingam@users.noreply.github.com>
Date: Tue, 30 Jun 2026 11:09:42 +0530
Subject: [PATCH 4/8] Updated the fix and test case.
---
.../EventCallbackFactoryBinderExtensions.cs | 36 +++++++------
...ventCallbackFactoryBinderExtensionsTest.cs | 52 +++++++++++++++++++
2 files changed, 72 insertions(+), 16 deletions(-)
diff --git a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs
index 2d2e1e4497ec..10a749ebd004 100644
--- a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs
+++ b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs
@@ -1371,14 +1371,9 @@ private static EventCallback CreateBinderCore(
{
setter(value!);
}
- else if (string.Empty.Equals(e.Value))
+ else if (string.Empty.Equals(e.Value) && ShouldSetDefaultOnEmptyString())
{
- var typeInfo = typeof(T);
- var isNullable = typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>);
- if (typeInfo == typeof(string) || isNullable)
- {
- setter(default!);
- }
+ setter(default!);
}
};
return factory.Create(receiver, callback);
@@ -1419,7 +1414,7 @@ private static EventCallback CreateBinderCoreAsync(
{
await setter(value!);
}
- else if (string.Empty.Equals(e.Value))
+ else if (string.Empty.Equals(e.Value) && ShouldSetDefaultOnEmptyString())
{
await setter(default!);
}
@@ -1463,14 +1458,9 @@ private static EventCallback CreateBinderCore(
{
setter(value!);
}
- else if (string.Empty.Equals(e.Value))
+ else if (string.Empty.Equals(e.Value) && ShouldSetDefaultOnEmptyString())
{
- var typeInfo = typeof(T);
- var isNullable = typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>);
- if (typeInfo == typeof(string) || isNullable)
- {
- setter(default!);
- }
+ setter(default!);
}
};
return factory.Create(receiver, callback);
@@ -1512,11 +1502,25 @@ private static EventCallback CreateBinderCoreAsync(
{
await setter(value!);
}
- else if (string.Empty.Equals(e.Value))
+ else if (string.Empty.Equals(e.Value) && ShouldSetDefaultOnEmptyString())
{
await setter(default!);
}
};
return factory.Create(receiver, callback);
}
+
+ // Determines whether an empty string input should reset the bound value to default(T).
+ // For most types an empty string maps to default(T) (e.g., string => null, int => 0, int? => null).
+ // However, non-nullable date/time types have no meaningful "empty" representation, so resetting them
+ // to default (e.g., DateTime.MinValue) would be surprising. For those we leave the existing value
+ // unchanged instead.
+ private static bool ShouldSetDefaultOnEmptyString()
+ {
+ var typeInfo = typeof(T);
+ return typeInfo != typeof(DateTime)
+ && typeInfo != typeof(DateTimeOffset)
+ && typeInfo != typeof(DateOnly)
+ && typeInfo != typeof(TimeOnly);
+ }
}
diff --git a/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs b/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs
index 80efbd8ba62a..dbd9ad966f2d 100644
--- a/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs
+++ b/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs
@@ -499,6 +499,58 @@ public async Task CreateBinder_NonNullableDateTime_EmptyValue_PreservesEachBound
Assert.Equal(2, component.Count);
}
+ [Fact]
+ public async Task CreateBinder_AsyncSetter_NonNullableDateTime_EmptyValue_DoesNotResetBoundValue()
+ {
+ // Regression test for https://github.com/dotnet/aspnetcore/issues/40660
+ // This covers the Func overload (CreateBinderCoreAsync), which must also
+ // avoid resetting a non-nullable DateTime to default(DateTime) when the browser
+ // briefly reports an empty value during editing.
+ var value = new DateTime(2022, 2, 10);
+ var component = new EventCountingComponent();
+ Func setter = (_) => { value = _; return Task.CompletedTask; };
+
+ var binder = EventCallback.Factory.CreateBinder(component, setter, value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
+
+ await binder.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });
+
+ Assert.Equal(new DateTime(2022, 2, 10), value);
+ Assert.Equal(1, component.Count);
+ }
+
+ [Fact]
+ public async Task CreateBinder_AsyncSetter_NonNullableDateTime_NoFormat_EmptyValue_DoesNotResetBoundValue()
+ {
+ // Same regression as above, but exercising the CreateBinderCoreAsync overload without a format.
+ var value = new DateTime(2022, 2, 10);
+ var component = new EventCountingComponent();
+ Func setter = (_) => { value = _; return Task.CompletedTask; };
+
+ var binder = EventCallback.Factory.CreateBinder(component, setter, value, CultureInfo.InvariantCulture);
+
+ await binder.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });
+
+ Assert.Equal(new DateTime(2022, 2, 10), value);
+ Assert.Equal(1, component.Count);
+ }
+
+ [Fact]
+ public async Task CreateBinder_AsyncSetter_NullableInt_EmptyValue_CallsSetterWithDefault()
+ {
+ // The async setter overload must still apply default(T) on empty string for types where
+ // that is the expected behavior (e.g., nullable types reset to null).
+ var value = (int?)17;
+ var component = new EventCountingComponent();
+ Func setter = (_) => { value = _; return Task.CompletedTask; };
+
+ var binder = EventCallback.Factory.CreateBinder(component, setter, value);
+
+ await binder.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });
+
+ Assert.Null(value);
+ Assert.Equal(1, component.Count);
+ }
+
[Fact]
public async Task CreateBinder_DateTimeOffset()
{
From d4aaaf4bf4bfa7004e8d7d1012322c9ec1d7a89d Mon Sep 17 00:00:00 2001
From: NanthiniMahalingam
<105482474+NanthiniMahalingam@users.noreply.github.com>
Date: Wed, 1 Jul 2026 15:29:21 +0530
Subject: [PATCH 5/8] Removed unwanted comment lines.
---
.../ServerFixtures/BlazorWasmTestAppFixture.cs | 5 -----
1 file changed, 5 deletions(-)
diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BlazorWasmTestAppFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BlazorWasmTestAppFixture.cs
index fcfddb631b7c..c1b177f2443f 100644
--- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BlazorWasmTestAppFixture.cs
+++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BlazorWasmTestAppFixture.cs
@@ -63,12 +63,7 @@ protected override IHost CreateWebHost()
}
var app = BlazorGateway.BuildWebHost(args.ToArray());
-
- // BlazorGateway serves individual static assets (e.g. index.html, _framework/*) but does not
- // register a default-document/SPA fallback. The E2E tests navigate to the app's path base
- // (e.g. /subdir), so fall back to index.html for requests that don't match a static asset.
app.MapFallbackToFile("index.html");
-
return app;
}
From cf144f7cc9585a9de417f7890a6da392138d6297 Mon Sep 17 00:00:00 2001
From: NanthiniMahalingam
<105482474+NanthiniMahalingam@users.noreply.github.com>
Date: Mon, 6 Jul 2026 16:08:19 +0530
Subject: [PATCH 6/8] Updated the test case.
---
.../BlazorWasmTestAppFixture.cs | 4 +-
.../test/E2ETest/Tests/FormsInputDateTest.cs | 47 +++++++++----------
.../BasicTestApp/BasicTestApp.csproj | 2 +
3 files changed, 24 insertions(+), 29 deletions(-)
diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BlazorWasmTestAppFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BlazorWasmTestAppFixture.cs
index c1b177f2443f..e0626d544fd6 100644
--- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BlazorWasmTestAppFixture.cs
+++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BlazorWasmTestAppFixture.cs
@@ -62,9 +62,7 @@ protected override IHost CreateWebHost()
args.Add(Environment);
}
- var app = BlazorGateway.BuildWebHost(args.ToArray());
- app.MapFallbackToFile("index.html");
- return app;
+ return BlazorGateway.BuildWebHost(args.ToArray());
}
private IHost CreateStaticWebHost(string contentRoot)
diff --git a/src/Components/test/E2ETest/Tests/FormsInputDateTest.cs b/src/Components/test/E2ETest/Tests/FormsInputDateTest.cs
index 9837872592ab..6a2fa662f32a 100644
--- a/src/Components/test/E2ETest/Tests/FormsInputDateTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsInputDateTest.cs
@@ -45,22 +45,21 @@ public void InputDateInteractsWithEditContext_NonNullableDateTime()
// Validates on edit
Browser.Equal("valid", () => renewalDateInput.GetDomAttribute("class"));
- renewalDateInput.SendKeys($"{Keys.Backspace}\t{Keys.Backspace}\t{Keys.Backspace}\t");
- renewalDateInput.SendKeys("01/01/2000\t");
+ SetDateInputValue(renewalDateInput, "2000-01-01");
Browser.Equal("modified valid", () => renewalDateInput.GetDomAttribute("class"));
- // Can become invalid
- renewalDateInput.SendKeys("11-11-11111\t");
+ // Can become invalid (year is out of range for DateTime)
+ SetDateInputValue(renewalDateInput, "11111-11-11");
Browser.Equal("modified invalid", () => renewalDateInput.GetDomAttribute("class"));
Browser.Equal(new[] { "The RenewalDate field must be a date." }, messagesAccessor);
// Empty is invalid, because it's not nullable
- renewalDateInput.SendKeys($"{Keys.Backspace}\t{Keys.Backspace}\t{Keys.Backspace}\t");
+ SetDateInputValue(renewalDateInput, "");
Browser.Equal("modified invalid", () => renewalDateInput.GetDomAttribute("class"));
Browser.Equal(new[] { "The RenewalDate field must be a date." }, messagesAccessor);
// Can become valid
- renewalDateInput.SendKeys("01/01/01\t");
+ SetDateInputValue(renewalDateInput, "2001-01-01");
Browser.Equal("modified valid", () => renewalDateInput.GetDomAttribute("class"));
Browser.Empty(messagesAccessor);
}
@@ -104,13 +103,13 @@ public void InputDateInteractsWithEditContext_TimeInput()
// Validates on edit
Browser.Equal("valid", () => departureTimeInput.GetDomAttribute("class"));
- departureTimeInput.SendKeys("06:43\t");
+ SetDateInputValue(departureTimeInput, "06:43");
Browser.Equal("modified valid", () => departureTimeInput.GetDomAttribute("class"));
// Can become invalid
// Stricly speaking the following is equivalent to the empty state, because that's how incomplete input is represented
// We don't know of any way to produce a different (non-empty-equivalent) state using UI gestures, so there's nothing else to test
- departureTimeInput.SendKeys($"20{Keys.Backspace}\t");
+ SetDateInputValue(departureTimeInput, "");
Browser.Equal("modified invalid", () => departureTimeInput.GetDomAttribute("class"));
Browser.Equal(new[] { "The DepartureTime field must be a time." }, messagesAccessor);
}
@@ -131,13 +130,12 @@ public void InputDateInteractsWithEditContext_TimeInput_Step()
// Input works with seconds value of zero and has the expected final value
Browser.Equal("valid", () => departureTimeInput.GetDomAttribute("class"));
- departureTimeInput.SendKeys("111111");
+ SetDateInputValue(departureTimeInput, "11:11:11");
Browser.Equal("modified valid", () => departureTimeInput.GetDomAttribute("class"));
Browser.Equal("11:11:11", () => departureTimeInput.GetDomProperty("value"));
// Input works with non-zero seconds value
- // Move to the beginning of the input and put the new time
- departureTimeInput.SendKeys(string.Concat(Enumerable.Repeat(Keys.ArrowLeft, 3)) + "101010");
+ SetDateInputValue(departureTimeInput, "10:10:10");
Browser.Equal("modified valid", () => departureTimeInput.GetDomAttribute("class"));
Browser.Equal("10:10:10", () => departureTimeInput.GetDomProperty("value"));
}
@@ -151,22 +149,21 @@ public void InputDateInteractsWithEditContext_MonthInput()
// Validates on edit
Browser.Equal("valid", () => visitMonthInput.GetDomAttribute("class"));
- visitMonthInput.SendKeys($"03{Keys.ArrowRight}2005\t");
+ SetDateInputValue(visitMonthInput, "2005-03");
Browser.Equal("modified valid", () => visitMonthInput.GetDomAttribute("class"));
// Empty is invalid because it's not nullable
- visitMonthInput.Clear();
+ SetDateInputValue(visitMonthInput, "");
Browser.Equal("modified invalid", () => visitMonthInput.GetDomAttribute("class"));
Browser.Equal(new[] { "The VisitMonth field must be a year and month." }, messagesAccessor);
- // Invalid year (11111)
- visitMonthInput.SendKeys($"11{Keys.ArrowRight}11111\t");
+ // Invalid year (11111, out of range for DateTime)
+ SetDateInputValue(visitMonthInput, "11111-11");
Browser.Equal("modified invalid", () => visitMonthInput.GetDomAttribute("class"));
Browser.Equal(new[] { "The VisitMonth field must be a year and month." }, messagesAccessor);
// Can become valid again
- visitMonthInput.Clear();
- visitMonthInput.SendKeys($"11{Keys.ArrowRight}1111\t");
+ SetDateInputValue(visitMonthInput, "1111-11");
Browser.Equal("modified valid", () => visitMonthInput.GetDomAttribute("class"));
Browser.Empty(messagesAccessor);
}
@@ -187,22 +184,21 @@ public void InputDateInteractsWithEditContext_DateTimeLocalInput()
// Validates on edit and has the expected value
Browser.Equal("valid", () => appointmentInput.GetDomAttribute("class"));
- appointmentInput.SendKeys($"01011970{Keys.ArrowRight}05421");
+ SetDateInputValue(appointmentInput, "1970-01-01T05:42");
Browser.Equal("modified valid", () => appointmentInput.GetDomAttribute("class"));
// Empty is invalid because it's not nullable
- appointmentInput.Clear();
+ SetDateInputValue(appointmentInput, "");
Browser.Equal("modified invalid", () => appointmentInput.GetDomAttribute("class"));
Browser.Equal(new[] { "The AppointmentDateAndTime field must be a date and time." }, messagesAccessor);
- // Invalid year (11111)
- appointmentInput.SendKeys($"111111111{Keys.ArrowRight}11111");
+ // Invalid year (11111, out of range for DateTime)
+ SetDateInputValue(appointmentInput, "11111-11-11T11:11");
Browser.Equal("modified invalid", () => appointmentInput.GetDomAttribute("class"));
Browser.Equal(new[] { "The AppointmentDateAndTime field must be a date and time." }, messagesAccessor);
// Can become valid again
- appointmentInput.Clear();
- appointmentInput.SendKeys($"11111111{Keys.ArrowRight}11111");
+ SetDateInputValue(appointmentInput, "1111-11-11T11:11");
Browser.Equal("modified valid", () => appointmentInput.GetDomAttribute("class"));
Browser.Empty(messagesAccessor);
}
@@ -223,13 +219,12 @@ public void InputDateInteractsWithEditContext_DateTimeLocalInput_Step()
// Input works with seconds value of zero (as in, starting from a zero value, which is the default) and has the expected final value
Browser.Equal("valid", () => appointmentInput.GetDomAttribute("class"));
- appointmentInput.SendKeys($"11111970{Keys.ArrowRight}114216");
+ SetDateInputValue(appointmentInput, "1970-11-11T11:42:16");
Browser.Equal("modified valid", () => appointmentInput.GetDomAttribute("class"));
Browser.Equal("1970-11-11T11:42:16", () => appointmentInput.GetDomProperty("value"));
// Input works when starting with a non-zero seconds value
- // Move to the beginning of the input and put the new value
- appointmentInput.SendKeys(string.Concat(Enumerable.Repeat(Keys.ArrowLeft, 6)) + $"10101970{Keys.ArrowRight}105321");
+ SetDateInputValue(appointmentInput, "1970-10-10T10:53:21");
Browser.Equal("modified valid", () => appointmentInput.GetDomAttribute("class"));
Browser.Equal("1970-10-10T10:53:21", () => appointmentInput.GetDomProperty("value"));
}
diff --git a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj
index 5057ae25d38f..6839322c64ae 100644
--- a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj
+++ b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj
@@ -12,6 +12,8 @@
true
+ true
+
true
From 6d7845bb519100317cc4efed7c4b828cc90b56c2 Mon Sep 17 00:00:00 2001
From: NanthiniMahalingam
<105482474+NanthiniMahalingam@users.noreply.github.com>
Date: Wed, 8 Jul 2026 12:23:55 +0530
Subject: [PATCH 7/8] Updated the comment.
---
src/Components/test/E2ETest/Tests/FormsInputDateTest.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Components/test/E2ETest/Tests/FormsInputDateTest.cs b/src/Components/test/E2ETest/Tests/FormsInputDateTest.cs
index 6a2fa662f32a..495edc8fd932 100644
--- a/src/Components/test/E2ETest/Tests/FormsInputDateTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsInputDateTest.cs
@@ -107,7 +107,7 @@ public void InputDateInteractsWithEditContext_TimeInput()
Browser.Equal("modified valid", () => departureTimeInput.GetDomAttribute("class"));
// Can become invalid
- // Stricly speaking the following is equivalent to the empty state, because that's how incomplete input is represented
+ // Strictly speaking the following is equivalent to the empty state, because that's how incomplete input is represented
// We don't know of any way to produce a different (non-empty-equivalent) state using UI gestures, so there's nothing else to test
SetDateInputValue(departureTimeInput, "");
Browser.Equal("modified invalid", () => departureTimeInput.GetDomAttribute("class"));
From 62aab9fff7f524d38d41f003f1a0bbeccba3784a Mon Sep 17 00:00:00 2001
From: NanthiniMahalingam
<105482474+NanthiniMahalingam@users.noreply.github.com>
Date: Wed, 8 Jul 2026 12:51:07 +0530
Subject: [PATCH 8/8] Reverted the unwanted code changes.
---
.../Components/src/BindConverter.cs | 26 ++----
.../EventCallbackFactoryBinderExtensions.cs | 22 +----
...ventCallbackFactoryBinderExtensionsTest.cs | 93 -------------------
3 files changed, 12 insertions(+), 129 deletions(-)
diff --git a/src/Components/Components/src/BindConverter.cs b/src/Components/Components/src/BindConverter.cs
index 5506df0a40e5..a70d06d0d1d7 100644
--- a/src/Components/Components/src/BindConverter.cs
+++ b/src/Components/Components/src/BindConverter.cs
@@ -380,11 +380,7 @@ private static string FormatDateTimeValueCore(DateTime value, CultureInfo? cultu
{
if (value == null)
{
- // Return an empty string (rather than null) so a null nullable date formats the same way an
- // empty date input reports its value to the server. This keeps the rendered 'value' attribute
- // in sync with what the browser already shows, preventing the diff from re-writing it and
- // resetting the caret/segments while the user is typing (e.g. ).
- return string.Empty;
+ return null;
}
if (format != null)
@@ -399,7 +395,7 @@ private static string FormatDateTimeValueCore(DateTime value, CultureInfo? cultu
{
if (value == null)
{
- return string.Empty;
+ return null;
}
return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
@@ -470,9 +466,7 @@ private static string FormatDateTimeOffsetValueCore(DateTimeOffset value, Cultur
{
if (value == null)
{
- // See FormatNullableDateTimeValueCore: a null value formats as empty so the rendered
- // 'value' attribute matches what an empty date/time input reports to the server.
- return string.Empty;
+ return null;
}
if (format != null)
@@ -487,7 +481,7 @@ private static string FormatDateTimeOffsetValueCore(DateTimeOffset value, Cultur
{
if (value == null)
{
- return string.Empty;
+ return null;
}
return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
@@ -559,9 +553,7 @@ private static string FormatDateOnlyValueCore(DateOnly value, CultureInfo? cultu
{
if (value == null)
{
- // See FormatNullableDateTimeValueCore: a null value formats as empty so the rendered
- // 'value' attribute matches what an empty date/time input reports to the server.
- return string.Empty;
+ return null;
}
if (format != null)
@@ -577,7 +569,7 @@ private static string FormatDateOnlyValueCore(DateOnly value, CultureInfo? cultu
{
if (value == null)
{
- return string.Empty;
+ return null;
}
return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
@@ -649,9 +641,7 @@ private static string FormatTimeOnlyValueCore(TimeOnly value, CultureInfo? cultu
{
if (value == null)
{
- // See FormatNullableDateTimeValueCore: a null value formats as empty so the rendered
- // 'value' attribute matches what an empty date/time input reports to the server.
- return string.Empty;
+ return null;
}
if (format != null)
@@ -667,7 +657,7 @@ private static string FormatTimeOnlyValueCore(TimeOnly value, CultureInfo? cultu
{
if (value == null)
{
- return string.Empty;
+ return null;
}
return value.Value.ToString(culture ?? CultureInfo.CurrentCulture);
diff --git a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs
index 10a749ebd004..1532ff08b326 100644
--- a/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs
+++ b/src/Components/Components/src/EventCallbackFactoryBinderExtensions.cs
@@ -1371,7 +1371,7 @@ private static EventCallback CreateBinderCore(
{
setter(value!);
}
- else if (string.Empty.Equals(e.Value) && ShouldSetDefaultOnEmptyString())
+ else if (string.Empty.Equals(e.Value))
{
setter(default!);
}
@@ -1414,7 +1414,7 @@ private static EventCallback CreateBinderCoreAsync(
{
await setter(value!);
}
- else if (string.Empty.Equals(e.Value) && ShouldSetDefaultOnEmptyString())
+ else if (string.Empty.Equals(e.Value))
{
await setter(default!);
}
@@ -1458,7 +1458,7 @@ private static EventCallback CreateBinderCore(
{
setter(value!);
}
- else if (string.Empty.Equals(e.Value) && ShouldSetDefaultOnEmptyString())
+ else if (string.Empty.Equals(e.Value))
{
setter(default!);
}
@@ -1502,25 +1502,11 @@ private static EventCallback CreateBinderCoreAsync(
{
await setter(value!);
}
- else if (string.Empty.Equals(e.Value) && ShouldSetDefaultOnEmptyString())
+ else if (string.Empty.Equals(e.Value))
{
await setter(default!);
}
};
return factory.Create(receiver, callback);
}
-
- // Determines whether an empty string input should reset the bound value to default(T).
- // For most types an empty string maps to default(T) (e.g., string => null, int => 0, int? => null).
- // However, non-nullable date/time types have no meaningful "empty" representation, so resetting them
- // to default (e.g., DateTime.MinValue) would be surprising. For those we leave the existing value
- // unchanged instead.
- private static bool ShouldSetDefaultOnEmptyString()
- {
- var typeInfo = typeof(T);
- return typeInfo != typeof(DateTime)
- && typeInfo != typeof(DateTimeOffset)
- && typeInfo != typeof(DateOnly)
- && typeInfo != typeof(TimeOnly);
- }
}
diff --git a/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs b/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs
index dbd9ad966f2d..f23af117c53e 100644
--- a/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs
+++ b/src/Components/Components/test/EventCallbackFactoryBinderExtensionsTest.cs
@@ -458,99 +458,6 @@ public async Task CreateBinder_NullableDateTime_Format()
Assert.Equal(1, component.Count);
}
- [Fact]
- public async Task CreateBinder_NonNullableDateTime_EmptyValue_DoesNotResetBoundValue()
- {
- // Regression test for https://github.com/dotnet/aspnetcore/issues/40660
- // While the user edits a bound to a non-nullable
- // DateTime, the browser briefly reports an empty value for the change event. The
- // binder must NOT reset the bound field to default(DateTime) (0001-01-01) in that
- // case, otherwise the component re-renders the default date and the partially
- // entered value is lost/reset.
- var value = new DateTime(2022, 2, 10);
- var component = new EventCountingComponent();
- Action setter = (_) => value = _;
-
- var binder = EventCallback.Factory.CreateBinder(component, setter, value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
-
- await binder.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });
-
- Assert.Equal(new DateTime(2022, 2, 10), value);
- Assert.Equal(1, component.Count);
- }
-
- [Fact]
- public async Task CreateBinder_NonNullableDateTime_EmptyValue_PreservesEachBoundValue()
- {
- var value = new DateTime(2022, 2, 10);
- var component = new EventCountingComponent();
- Action setter = (_) => value = _;
-
- EventCallback binder = EventCallback.Factory.CreateBinder(component, setter, value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
- await binder.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });
- Assert.Equal(1, component.Count);
- var value1 = new DateTime(2023, 02, 09);
- Action setter1 = (_) => value1 = _;
-
- var binder1 = EventCallback.Factory.CreateBinder(component, setter1, value1, "yyyy-MM-dd", CultureInfo.InvariantCulture);
- await binder1.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });
- // The setter must not have been called, so the previous valid value is preserved.
- Assert.Equal(new DateTime(2023, 02, 9), value1);
- Assert.Equal(2, component.Count);
- }
-
- [Fact]
- public async Task CreateBinder_AsyncSetter_NonNullableDateTime_EmptyValue_DoesNotResetBoundValue()
- {
- // Regression test for https://github.com/dotnet/aspnetcore/issues/40660
- // This covers the Func overload (CreateBinderCoreAsync), which must also
- // avoid resetting a non-nullable DateTime to default(DateTime) when the browser
- // briefly reports an empty value during editing.
- var value = new DateTime(2022, 2, 10);
- var component = new EventCountingComponent();
- Func setter = (_) => { value = _; return Task.CompletedTask; };
-
- var binder = EventCallback.Factory.CreateBinder(component, setter, value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
-
- await binder.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });
-
- Assert.Equal(new DateTime(2022, 2, 10), value);
- Assert.Equal(1, component.Count);
- }
-
- [Fact]
- public async Task CreateBinder_AsyncSetter_NonNullableDateTime_NoFormat_EmptyValue_DoesNotResetBoundValue()
- {
- // Same regression as above, but exercising the CreateBinderCoreAsync overload without a format.
- var value = new DateTime(2022, 2, 10);
- var component = new EventCountingComponent();
- Func setter = (_) => { value = _; return Task.CompletedTask; };
-
- var binder = EventCallback.Factory.CreateBinder(component, setter, value, CultureInfo.InvariantCulture);
-
- await binder.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });
-
- Assert.Equal(new DateTime(2022, 2, 10), value);
- Assert.Equal(1, component.Count);
- }
-
- [Fact]
- public async Task CreateBinder_AsyncSetter_NullableInt_EmptyValue_CallsSetterWithDefault()
- {
- // The async setter overload must still apply default(T) on empty string for types where
- // that is the expected behavior (e.g., nullable types reset to null).
- var value = (int?)17;
- var component = new EventCountingComponent();
- Func setter = (_) => { value = _; return Task.CompletedTask; };
-
- var binder = EventCallback.Factory.CreateBinder(component, setter, value);
-
- await binder.InvokeAsync(new ChangeEventArgs() { Value = string.Empty, });
-
- Assert.Null(value);
- Assert.Equal(1, component.Count);
- }
-
[Fact]
public async Task CreateBinder_DateTimeOffset()
{