From 1993e73d27d0fa894fbe3a01219f905df9b4203d Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 6 Aug 2026 14:50:33 +0800 Subject: [PATCH 1/9] feat(BootstrapBlazorOptions): add DateTimeSettings settings --- .../Options/BootstrapBlazorOptions.cs | 6 +++++ .../Options/DateTimeSettings.cs | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/BootstrapBlazor/Options/DateTimeSettings.cs diff --git a/src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs b/src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs index af600ddf312..fe32dfac25e 100644 --- a/src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs +++ b/src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs @@ -190,6 +190,12 @@ public class BootstrapBlazorOptions : IOptions /// public int ChangeDetectionTaskInterval { get; set; } = 5000; + /// + /// 获得/设置 配置实例 + /// Gets or sets the configuration instance + /// + public DateTimeSettings DateTimeSettings { get; set; } = new(); + BootstrapBlazorOptions IOptions.Value => this; /// diff --git a/src/BootstrapBlazor/Options/DateTimeSettings.cs b/src/BootstrapBlazor/Options/DateTimeSettings.cs new file mode 100644 index 00000000000..6aa363b4b71 --- /dev/null +++ b/src/BootstrapBlazor/Options/DateTimeSettings.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Components; + +/// +/// DateTime 相关全局配置类 +/// +public class DateTimeSettings +{ + /// + /// 自定义解析日期时间的方法 + /// + public Func? ParseDateTimeResolve { get; set; } + + /// + /// 自定义解析日期时间偏移的方法 + /// + public Func? ParseDateTimeOffsetResolve { get; set; } +} From 1079f2b6c9fa689efff444eae0b3d8a721230ea6 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 6 Aug 2026 15:13:48 +0800 Subject: [PATCH 2/9] feat: add ParseDateTimeResolve parameter --- .../DateTimePicker/DateTimePicker.razor.cs | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs index 1f7a774fd46..7ed2fe1ff23 100644 --- a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs +++ b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs @@ -336,6 +336,18 @@ public string? Format [Parameter] public Func? OnBlurAsync { get; set; } + /// + /// 自定义解析日期时间的方法 + /// Custom method to parse date and time + /// + public Func? ParseDateTimeResolve { get; set; } + + /// + /// 自定义解析日期时间偏移的方法 + /// Custom method to parse date and time offset + /// + public Func? ParseDateTimeOffsetResolve { get; set; } + [Inject] [NotNull] private IStringLocalizer>? Localizer { get; set; } @@ -344,6 +356,10 @@ public string? Format [NotNull] private IIconTheme? IconTheme { get; set; } + [Inject] + [NotNull] + private IOptionsMonitor? Options { get; set; } + [NotNull] private string? GenericTypeErrorMessage { get; set; } @@ -570,14 +586,44 @@ protected override bool TryParseValueFromString(string value, [MaybeNullWhen(fal { result = default; validationErrorMessage = null; - var ret = DateTimeHelper.TryToDateTime(value, out var val); + var ret = Value is DateTime ? TryParseDateTime(value, out var val) : TryParseDateTimeOffset(value, out val); if (ret) { - result = (TValue)(object)val; + result = (TValue)val; + } + return ret; + } + + private bool TryParseDateTime(string value, out object val) + { + var op = Options.CurrentValue; + var resolve = ParseDateTimeResolve ?? op.DateTimeSettings.ParseDateTimeResolve; + if (resolve != null) + { + val = resolve(value); + return true; } + + var ret = DateTimeHelper.TryToDateTime(value, out var d); + val = ret ? d : DateTime.MinValue; return ret; } + private bool TryParseDateTimeOffset(string value, out object val) + { + var op = Options.CurrentValue; + var resolve = ParseDateTimeOffsetResolve ?? op.DateTimeSettings.ParseDateTimeOffsetResolve; + if (resolve != null) + { + val = resolve(value); + return true; + } + + var v = DateTimeHelper.ToDateTimeOffset(value); + val = v ?? DateTimeOffset.MinValue; + return v != null; + } + private string? ReadonlyString => IsEditable ? null : "readonly"; /// From dd921df107ed9d5030277d0a314436abb1e19071 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 6 Aug 2026 15:16:20 +0800 Subject: [PATCH 3/9] =?UTF-8?q?doc:=20=E5=A2=9E=E5=8A=A0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs b/src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs index fe32dfac25e..192d96c0b24 100644 --- a/src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs +++ b/src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs @@ -193,6 +193,7 @@ public class BootstrapBlazorOptions : IOptions /// /// 获得/设置 配置实例 /// Gets or sets the configuration instance + /// v10.9.1 /// public DateTimeSettings DateTimeSettings { get; set; } = new(); From 38ff62c046ccfe07bf49127e2393d6755423e0c8 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 6 Aug 2026 15:16:31 +0800 Subject: [PATCH 4/9] =?UTF-8?q?doc:=20=E5=A2=9E=E5=8A=A0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/DateTimePicker/DateTimePicker.razor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs index 7ed2fe1ff23..bf4d356c95d 100644 --- a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs +++ b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs @@ -339,12 +339,14 @@ public string? Format /// /// 自定义解析日期时间的方法 /// Custom method to parse date and time + /// v10.9.1 /// public Func? ParseDateTimeResolve { get; set; } /// /// 自定义解析日期时间偏移的方法 /// Custom method to parse date and time offset + /// v10.9.1 /// public Func? ParseDateTimeOffsetResolve { get; set; } From 4c19f6467a9bc18afe2987604e38b49b3c527b04 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 6 Aug 2026 15:27:35 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20parameter=20?= =?UTF-8?q?=E5=85=B3=E9=94=AE=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/DateTimePicker/DateTimePicker.razor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs index bf4d356c95d..dced0305a29 100644 --- a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs +++ b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs @@ -341,6 +341,7 @@ public string? Format /// Custom method to parse date and time /// v10.9.1 /// + [Parameter] public Func? ParseDateTimeResolve { get; set; } /// @@ -348,6 +349,7 @@ public string? Format /// Custom method to parse date and time offset /// v10.9.1 /// + [Parameter] public Func? ParseDateTimeOffsetResolve { get; set; } [Inject] From 72193dcd326557f9f6ed617bad0743acf8785e94 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 6 Aug 2026 15:27:48 +0800 Subject: [PATCH 6/9] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=AD=A3=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/DateTimePicker/DateTimePicker.razor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs index dced0305a29..f7361db4ab6 100644 --- a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs +++ b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs @@ -590,7 +590,7 @@ protected override bool TryParseValueFromString(string value, [MaybeNullWhen(fal { result = default; validationErrorMessage = null; - var ret = Value is DateTime ? TryParseDateTime(value, out var val) : TryParseDateTimeOffset(value, out val); + var ret = ValueType == typeof(DateTime) ? TryParseDateTime(value, out var val) : TryParseDateTimeOffset(value, out val); if (ret) { result = (TValue)val; From a12991f4ca42866f1d557be2cc27c27dcdbfce8e Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 6 Aug 2026 15:27:55 +0800 Subject: [PATCH 7/9] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnitTest/Components/DateTimePickerTest.cs | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/test/UnitTest/Components/DateTimePickerTest.cs b/test/UnitTest/Components/DateTimePickerTest.cs index eb848ba4541..40cd1bb5b2a 100644 --- a/test/UnitTest/Components/DateTimePickerTest.cs +++ b/test/UnitTest/Components/DateTimePickerTest.cs @@ -1425,6 +1425,153 @@ public async Task OnBlurAsync_Ok() Assert.True(blur); } + [Fact] + public async Task TryParseValueFromString_NullableDateTime_Ok() + { + // Value 为 null 时应按泛型类型走 DateTime 解析分支 + var cut = Context.Render>(pb => + { + pb.Add(a => a.IsEditable, true); + pb.Add(a => a.ViewMode, DatePickerViewMode.Date); + pb.Add(a => a.DateFormat, "MM/dd/yyyy"); + pb.Add(a => a.Value, null); + }); + var input = cut.Find(".datetime-picker-input"); + await cut.InvokeAsync(() => input.Change("02/15/2024")); + Assert.Equal(new DateTime(2024, 2, 15), cut.Instance.Value); + + // 非法数值组件值保持不变 + await cut.InvokeAsync(() => input.Change("test")); + Assert.Equal(new DateTime(2024, 2, 15), cut.Instance.Value); + } + + [Fact] + public async Task TryParseValueFromString_DateTimeOffset_Ok() + { + var cut = Context.Render>(pb => + { + pb.Add(a => a.IsEditable, true); + pb.Add(a => a.ViewMode, DatePickerViewMode.Date); + pb.Add(a => a.DateFormat, "MM/dd/yyyy"); + pb.Add(a => a.Value, DateTimeOffset.MinValue); + }); + var input = cut.Find(".datetime-picker-input"); + + // 无时区信息按本地时区补齐偏移 + await cut.InvokeAsync(() => input.Change("02/15/2024")); + Assert.Equal(new DateTimeOffset(new DateTime(2024, 2, 15, 0, 0, 0, DateTimeKind.Local)), cut.Instance.Value); + + // 非法数值组件值保持不变 + await cut.InvokeAsync(() => input.Change("test")); + Assert.Equal(new DateTimeOffset(new DateTime(2024, 2, 15, 0, 0, 0, DateTimeKind.Local)), cut.Instance.Value); + } + + [Fact] + public async Task TryParseValueFromString_NullableDateTimeOffset_Ok() + { + // Value 为 null 时应按泛型类型走 DateTimeOffset 解析分支 + var cut = Context.Render>(pb => + { + pb.Add(a => a.IsEditable, true); + pb.Add(a => a.ViewMode, DatePickerViewMode.Date); + pb.Add(a => a.DateFormat, "MM/dd/yyyy"); + pb.Add(a => a.Value, null); + }); + var input = cut.Find(".datetime-picker-input"); + await cut.InvokeAsync(() => input.Change("02/15/2024")); + Assert.Equal(new DateTimeOffset(new DateTime(2024, 2, 15, 0, 0, 0, DateTimeKind.Local)), cut.Instance.Value); + } + + [Fact] + public async Task ParseDateTimeResolve_Parameter_Ok() + { + // 自定义解析方法优先级高于内置解析 + var cut = Context.Render>(pb => + { + pb.Add(a => a.IsEditable, true); + pb.Add(a => a.ViewMode, DatePickerViewMode.Date); + pb.Add(a => a.DateFormat, "MM/dd/yyyy"); + pb.Add(a => a.ParseDateTimeResolve, v => new DateTime(2020, 1, 1)); + }); + var input = cut.Find(".datetime-picker-input"); + + // 传入非法字符串同样返回自定义解析结果 + await cut.InvokeAsync(() => input.Change("test")); + Assert.Equal(new DateTime(2020, 1, 1), cut.Instance.Value); + } + + [Fact] + public async Task ParseDateTimeResolve_Options_Ok() + { + // 全局配置自定义解析方法 + var options = Context.Services.GetRequiredService>(); + options.CurrentValue.DateTimeSettings.ParseDateTimeResolve = v => new DateTime(2021, 2, 2); + + var cut = Context.Render>(pb => + { + pb.Add(a => a.IsEditable, true); + pb.Add(a => a.ViewMode, DatePickerViewMode.Date); + pb.Add(a => a.DateFormat, "MM/dd/yyyy"); + }); + var input = cut.Find(".datetime-picker-input"); + await cut.InvokeAsync(() => input.Change("test")); + Assert.Equal(new DateTime(2021, 2, 2), cut.Instance.Value); + + // 组件参数优先级高于全局配置 + cut.Render(pb => + { + pb.Add(a => a.ParseDateTimeResolve, v => new DateTime(2022, 3, 3)); + }); + await cut.InvokeAsync(() => input.Change("test")); + Assert.Equal(new DateTime(2022, 3, 3), cut.Instance.Value); + } + + [Fact] + public async Task ParseDateTimeOffsetResolve_Parameter_Ok() + { + var expected = new DateTimeOffset(new DateTime(2020, 1, 1), TimeSpan.FromHours(8)); + var cut = Context.Render>(pb => + { + pb.Add(a => a.IsEditable, true); + pb.Add(a => a.ViewMode, DatePickerViewMode.Date); + pb.Add(a => a.DateFormat, "MM/dd/yyyy"); + pb.Add(a => a.ParseDateTimeOffsetResolve, v => expected); + }); + var input = cut.Find(".datetime-picker-input"); + + // 传入非法字符串同样返回自定义解析结果 + await cut.InvokeAsync(() => input.Change("test")); + Assert.Equal(expected, cut.Instance.Value); + } + + [Fact] + public async Task ParseDateTimeOffsetResolve_Options_Ok() + { + // 全局配置自定义解析方法 + var expected = new DateTimeOffset(new DateTime(2021, 2, 2), TimeSpan.FromHours(8)); + var options = Context.Services.GetRequiredService>(); + options.CurrentValue.DateTimeSettings.ParseDateTimeOffsetResolve = v => expected; + + var cut = Context.Render>(pb => + { + pb.Add(a => a.IsEditable, true); + pb.Add(a => a.ViewMode, DatePickerViewMode.Date); + pb.Add(a => a.DateFormat, "MM/dd/yyyy"); + }); + var input = cut.Find(".datetime-picker-input"); + await cut.InvokeAsync(() => input.Change("test")); + Assert.Equal(expected, cut.Instance.Value); + + // 组件参数优先级高于全局配置 + var expectedParameter = new DateTimeOffset(new DateTime(2022, 3, 3), TimeSpan.FromHours(8)); + cut.Render(pb => + { + pb.Add(a => a.ParseDateTimeOffsetResolve, v => expectedParameter); + }); + await cut.InvokeAsync(() => input.Change("test")); + Assert.Equal(expectedParameter, cut.Instance.Value); + } + class MockDateTimePicker : DatePickerBody { public static bool GetSafeYearDateTime_Ok() From d28764104d6a36356372d49a0686319174eff663 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 6 Aug 2026 15:36:37 +0800 Subject: [PATCH 8/9] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E7=A9=BA=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Components/DateTimePickerTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/UnitTest/Components/DateTimePickerTest.cs b/test/UnitTest/Components/DateTimePickerTest.cs index 40cd1bb5b2a..4fd607b61b5 100644 --- a/test/UnitTest/Components/DateTimePickerTest.cs +++ b/test/UnitTest/Components/DateTimePickerTest.cs @@ -4,6 +4,7 @@ // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone using AngleSharp.Dom; +using Microsoft.Extensions.Options; namespace UnitTest.Components; From 6403d1fc2325bc3f2f963938259b5acc21bfe5b0 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Thu, 6 Aug 2026 15:36:47 +0800 Subject: [PATCH 9/9] chore: bump version 10.9.1 --- src/BootstrapBlazor/BootstrapBlazor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index 2b61a006de0..bedc64e8a22 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@  - 10.9.1-beta03 + 10.9.1