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 diff --git a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs index 1f7a774fd46..f7361db4ab6 100644 --- a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs +++ b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs @@ -336,6 +336,22 @@ public string? Format [Parameter] public Func? OnBlurAsync { get; set; } + /// + /// 自定义解析日期时间的方法 + /// Custom method to parse date and time + /// v10.9.1 + /// + [Parameter] + public Func? ParseDateTimeResolve { get; set; } + + /// + /// 自定义解析日期时间偏移的方法 + /// Custom method to parse date and time offset + /// v10.9.1 + /// + [Parameter] + public Func? ParseDateTimeOffsetResolve { get; set; } + [Inject] [NotNull] private IStringLocalizer>? Localizer { get; set; } @@ -344,6 +360,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 +590,44 @@ protected override bool TryParseValueFromString(string value, [MaybeNullWhen(fal { result = default; validationErrorMessage = null; - var ret = DateTimeHelper.TryToDateTime(value, out var val); + var ret = ValueType == typeof(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"; /// diff --git a/src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs b/src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs index af600ddf312..192d96c0b24 100644 --- a/src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs +++ b/src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs @@ -190,6 +190,13 @@ public class BootstrapBlazorOptions : IOptions /// public int ChangeDetectionTaskInterval { get; set; } = 5000; + /// + /// 获得/设置 配置实例 + /// Gets or sets the configuration instance + /// v10.9.1 + /// + 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; } +} diff --git a/test/UnitTest/Components/DateTimePickerTest.cs b/test/UnitTest/Components/DateTimePickerTest.cs index eb848ba4541..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; @@ -1425,6 +1426,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()