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() {