Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/BootstrapBlazor.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>10.9.1-beta03</Version>
<Version>10.9.1</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,22 @@ public string? Format
[Parameter]
public Func<TValue, Task>? OnBlurAsync { get; set; }

/// <summary>
/// <para lang="zh">自定义解析日期时间的方法</para>
/// <para lang="en">Custom method to parse date and time</para>
/// <para>v<version>10.9.1</version></para>
/// </summary>
[Parameter]
public Func<string, DateTime>? ParseDateTimeResolve { get; set; }

/// <summary>
/// <para lang="zh">自定义解析日期时间偏移的方法</para>
/// <para lang="en">Custom method to parse date and time offset</para>
/// <para>v<version>10.9.1</version></para>
/// </summary>
[Parameter]
public Func<string, DateTimeOffset>? ParseDateTimeOffsetResolve { get; set; }

[Inject]
[NotNull]
private IStringLocalizer<DateTimePicker<DateTime>>? Localizer { get; set; }
Expand All @@ -344,6 +360,10 @@ public string? Format
[NotNull]
private IIconTheme? IconTheme { get; set; }

[Inject]
[NotNull]
private IOptionsMonitor<BootstrapBlazorOptions>? Options { get; set; }

[NotNull]
private string? GenericTypeErrorMessage { get; set; }

Expand Down Expand Up @@ -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";

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,13 @@ public class BootstrapBlazorOptions : IOptions<BootstrapBlazorOptions>
/// </summary>
public int ChangeDetectionTaskInterval { get; set; } = 5000;

/// <summary>
/// <para lang="zh">获得/设置 <see cref="DateTimeSettings"/> 配置实例</para>
/// <para lang="en">Gets or sets the <see cref="DateTimeSettings"/> configuration instance</para>
/// <para>v<version>10.9.1</version></para>
/// </summary>
public DateTimeSettings DateTimeSettings { get; set; } = new();
Comment thread
ArgoZhang marked this conversation as resolved.

BootstrapBlazorOptions IOptions<BootstrapBlazorOptions>.Value => this;

/// <summary>
Expand Down
22 changes: 22 additions & 0 deletions src/BootstrapBlazor/Options/DateTimeSettings.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// <para lang="zh">DateTime 相关全局配置类</para>
/// </summary>
public class DateTimeSettings
{
/// <summary>
/// <para lang="zh">自定义解析日期时间的方法</para>
/// </summary>
public Func<string, DateTime>? ParseDateTimeResolve { get; set; }

/// <summary>
/// <para lang="zh">自定义解析日期时间偏移的方法</para>
/// </summary>
public Func<string, DateTimeOffset>? ParseDateTimeOffsetResolve { get; set; }
}
148 changes: 148 additions & 0 deletions test/UnitTest/Components/DateTimePickerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<DateTimePicker<DateTime?>>(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<DateTimePicker<DateTimeOffset>>(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<DateTimePicker<DateTimeOffset?>>(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<DateTimePicker<DateTime>>(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<IOptionsMonitor<BootstrapBlazorOptions>>();
options.CurrentValue.DateTimeSettings.ParseDateTimeResolve = v => new DateTime(2021, 2, 2);

var cut = Context.Render<DateTimePicker<DateTime>>(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<DateTimePicker<DateTimeOffset>>(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<IOptionsMonitor<BootstrapBlazorOptions>>();
options.CurrentValue.DateTimeSettings.ParseDateTimeOffsetResolve = v => expected;

var cut = Context.Render<DateTimePicker<DateTimeOffset>>(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()
Expand Down
Loading