From b480fa8aab61afe7cecfe3844ad97fea2fbc7692 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 7 Jun 2026 15:01:55 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=20IsNullable?= =?UTF-8?q?=20=E6=96=B9=E6=B3=95=E7=B2=BE=E7=AE=80=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AutoFill/AutoFill.razor.cs | 4 +--- .../Components/DateTimePicker/DateTimePicker.razor.cs | 2 +- src/BootstrapBlazor/Components/Display/DisplayBase.cs | 10 ++++++++++ .../InputNumber/BootstrapInputNumber.razor.cs | 2 +- .../Components/Radio/RadioList.razor.cs | 2 +- .../Components/Radio/RadioListGeneric.razor.cs | 2 +- src/BootstrapBlazor/Components/Select/Select.razor.cs | 2 +- src/BootstrapBlazor/Components/Select/SelectBase.cs | 4 +--- .../Components/SelectGeneric/SelectGeneric.razor.cs | 2 +- .../Components/Validate/ValidateBase.cs | 4 ++-- 10 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/BootstrapBlazor/Components/AutoFill/AutoFill.razor.cs b/src/BootstrapBlazor/Components/AutoFill/AutoFill.razor.cs index 4e96805bb92..dc9f02e1ca1 100644 --- a/src/BootstrapBlazor/Components/AutoFill/AutoFill.razor.cs +++ b/src/BootstrapBlazor/Components/AutoFill/AutoFill.razor.cs @@ -241,9 +241,7 @@ public void TriggerChange(string v) _clientValue = v; } - private bool IsNullable() => !ValueType.IsValueType || NullableUnderlyingType != null; - - private bool GetClearable() => IsClearable && !IsDisabled && IsNullable(); + private bool GetClearable() => IsClearable && !IsDisabled && (!ValueType.IsValueType || IsNullable()); private async Task OnClickItem(TValue val) { diff --git a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs index a00945c8bc9..d56a38afd4d 100644 --- a/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs +++ b/src/BootstrapBlazor/Components/DateTimePicker/DateTimePicker.razor.cs @@ -338,7 +338,7 @@ protected override void OnInitialized() base.OnInitialized(); // 泛型设置为可为空 - AllowNull = NullableUnderlyingType != null; + AllowNull = IsNullable(); } /// diff --git a/src/BootstrapBlazor/Components/Display/DisplayBase.cs b/src/BootstrapBlazor/Components/Display/DisplayBase.cs index 68bdeb52928..7aa1b44a63e 100644 --- a/src/BootstrapBlazor/Components/Display/DisplayBase.cs +++ b/src/BootstrapBlazor/Components/Display/DisplayBase.cs @@ -36,6 +36,16 @@ public abstract class DisplayBase : BootstrapModuleComponentBase /// protected static readonly Type? NullableUnderlyingType = Nullable.GetUnderlyingType(typeof(TValue)); + /// + /// 判断泛型参数 是否为 可空值类型 + /// Determines whether the generic parameter is a value type. + /// + /// + /// 表示 否则返回 + /// when is ; otherwise . + /// + protected bool IsNullable() => NullableUnderlyingType != null; + /// /// 获得/设置 泛型参数 TValue 可为空类型 Type 实例 /// Gets or sets Generic Parameter TValue Nullable Type Instance diff --git a/src/BootstrapBlazor/Components/InputNumber/BootstrapInputNumber.razor.cs b/src/BootstrapBlazor/Components/InputNumber/BootstrapInputNumber.razor.cs index 6a74774de51..0b73672aaec 100644 --- a/src/BootstrapBlazor/Components/InputNumber/BootstrapInputNumber.razor.cs +++ b/src/BootstrapBlazor/Components/InputNumber/BootstrapInputNumber.razor.cs @@ -280,7 +280,7 @@ protected override async Task OnBlur() CurrentValue = default!; } - if (NullableUnderlyingType != null && string.IsNullOrEmpty(CurrentValueAsString)) + if (IsNullable() && string.IsNullOrEmpty(CurrentValueAsString)) { // set component value empty await InvokeVoidAsync("clear", Id); diff --git a/src/BootstrapBlazor/Components/Radio/RadioList.razor.cs b/src/BootstrapBlazor/Components/Radio/RadioList.razor.cs index d5fc6ee9272..1209e267893 100644 --- a/src/BootstrapBlazor/Components/Radio/RadioList.razor.cs +++ b/src/BootstrapBlazor/Components/Radio/RadioList.razor.cs @@ -63,7 +63,7 @@ protected override void OnParametersSet() var t = ValueType; if (t.IsEnum && Items == null) { - Items = t.ToSelectList((NullableUnderlyingType != null && IsAutoAddNullItem) ? new SelectedItem("", NullItemText) : null); + Items = t.ToSelectList((IsNullable() && IsAutoAddNullItem) ? new SelectedItem("", NullItemText) : null); } base.OnParametersSet(); diff --git a/src/BootstrapBlazor/Components/Radio/RadioListGeneric.razor.cs b/src/BootstrapBlazor/Components/Radio/RadioListGeneric.razor.cs index a3982ba4062..61e359b8191 100644 --- a/src/BootstrapBlazor/Components/Radio/RadioListGeneric.razor.cs +++ b/src/BootstrapBlazor/Components/Radio/RadioListGeneric.razor.cs @@ -131,7 +131,7 @@ protected override void OnParametersSet() var t = ValueType; if (t.IsEnum && Items == null) { - Items = t.ToSelectList((NullableUnderlyingType != null && IsAutoAddNullItem) ? new SelectedItem(default!, NullItemText) : null); + Items = t.ToSelectList((IsNullable() && IsAutoAddNullItem) ? new SelectedItem(default!, NullItemText) : null); } Items ??= []; diff --git a/src/BootstrapBlazor/Components/Select/Select.razor.cs b/src/BootstrapBlazor/Components/Select/Select.razor.cs index a9707b4f88b..01cc586ae50 100644 --- a/src/BootstrapBlazor/Components/Select/Select.razor.cs +++ b/src/BootstrapBlazor/Components/Select/Select.razor.cs @@ -247,7 +247,7 @@ protected override async Task OnParametersSetAsync() if (!Items.Any() && ValueType.IsEnum()) { - var item = NullableUnderlyingType == null ? "" : PlaceHolder; + var item = IsNullable() ? PlaceHolder : ""; Items = ValueType.ToSelectList(string.IsNullOrEmpty(item) ? null : new SelectedItem("", item)); } diff --git a/src/BootstrapBlazor/Components/Select/SelectBase.cs b/src/BootstrapBlazor/Components/Select/SelectBase.cs index 29e51555f40..8c2aed8c6a1 100644 --- a/src/BootstrapBlazor/Components/Select/SelectBase.cs +++ b/src/BootstrapBlazor/Components/Select/SelectBase.cs @@ -220,13 +220,11 @@ protected override void OnParametersSet() /// A representing the asynchronous operation. public Task Hide() => InvokeVoidAsync("hide", Id); - private bool IsNullable() => !ValueType.IsValueType || NullableUnderlyingType != null; - /// /// 获得 是否显示清除按钮 /// Gets whether show the clear button /// - protected bool GetClearable() => IsClearable && !IsDisabled && IsNullable(); + protected bool GetClearable() => IsClearable && !IsDisabled && (!ValueType.IsValueType || IsNullable()); /// /// 清除搜索文本 diff --git a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs index 9fbd06e166e..9e3bd44d209 100644 --- a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs +++ b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs @@ -279,7 +279,7 @@ protected override void OnParametersSet() if (!Items.Any() && ValueType.IsEnum()) { - var item = NullableUnderlyingType == null ? "" : PlaceHolder; + var item = IsNullable() ? PlaceHolder : ""; Items = ValueType.ToSelectList(string.IsNullOrEmpty(item) ? null : new SelectedItem(default!, item)); } diff --git a/src/BootstrapBlazor/Components/Validate/ValidateBase.cs b/src/BootstrapBlazor/Components/Validate/ValidateBase.cs index 368ec610a2f..ba937e5f7dc 100644 --- a/src/BootstrapBlazor/Components/Validate/ValidateBase.cs +++ b/src/BootstrapBlazor/Components/Validate/ValidateBase.cs @@ -112,7 +112,7 @@ protected string CurrentValueAsString { _parsingValidationMessages?.Clear(); - if (NullableUnderlyingType != null && string.IsNullOrEmpty(value)) + if (IsNullable() && string.IsNullOrEmpty(value)) { // Assume if it's a nullable type, null/empty inputs should correspond to default(T) // Then all subclasses get nullable support almost automatically (they just have to @@ -498,7 +498,7 @@ private void ValidateType(ValidationContext context, List resu { // 增加数据基础类型验证 如泛型约定为 int 文本框值为 Empty // 可为空泛型约束时不检查 - if (NullableUnderlyingType == null && PreviousParsingAttemptFailed) + if (!IsNullable() && PreviousParsingAttemptFailed) { var memberNames = new string[] { context.MemberName! }; results.Add(new ValidationResult(PreviousErrorMessage, memberNames)); From 6b609486ad1e5ae84d9861e3db707d961de593db Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 7 Jun 2026 15:02:02 +0800 Subject: [PATCH 2/3] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=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 --- test/UnitTest/Components/SelectTest.cs | 76 -------------------------- 1 file changed, 76 deletions(-) diff --git a/test/UnitTest/Components/SelectTest.cs b/test/UnitTest/Components/SelectTest.cs index d7fa414cb1d..4f2bec7f3f3 100644 --- a/test/UnitTest/Components/SelectTest.cs +++ b/test/UnitTest/Components/SelectTest.cs @@ -229,82 +229,6 @@ public async Task IsClearable_Ok() cut1.DoesNotContain("clear-icon"); } - [Fact] - public void IsNullable_Ok() - { - var cut = Context.Render>(pb => - { - pb.Add(a => a.Items, new List() - { - new("", "请选择"), - new("2", "Test2"), - new("3", "Test3") - }); - }); - Assert.True(IsNullable(cut.Instance)); - - var cut1 = Context.Render>(pb => - { - pb.Add(a => a.Items, new List() - { - new("", "请选择"), - new("2", "Test2"), - new("3", "Test3") - }); - }); - Assert.True(IsNullable(cut1.Instance)); - - var cut2 = Context.Render>(pb => - { - pb.Add(a => a.Items, new List() - { - new("", "请选择"), - new("2", "Test2"), - new("3", "Test3") - }); - }); - Assert.True(IsNullable(cut2.Instance)); - - var cut3 = Context.Render>(pb => - { - pb.Add(a => a.Items, new List() - { - new("", "请选择"), - new("2", "Test2"), - new("3", "Test3") - }); - }); - Assert.True(IsNullable(cut3.Instance)); - - var cut4 = Context.Render>(pb => - { - pb.Add(a => a.Items, new List() - { - new("", "请选择"), - new("2", "Test2"), - new("3", "Test3") - }); - }); - Assert.False(IsNullable(cut4.Instance)); - - var cut5 = Context.Render>(pb => - { - pb.Add(a => a.Items, new List() - { - new("", "请选择"), - new("2", "Test2"), - new("3", "Test3") - }); - }); - Assert.True(IsNullable(cut5.Instance)); - } - - private static bool IsNullable(object select) - { - var mi = select.GetType().BaseType!.BaseType!.GetMethod("IsNullable", BindingFlags.Instance | BindingFlags.NonPublic)!; - return (bool)mi.Invoke(select, null)!; - } - [Fact] public void SelectOption_Ok() { From c53f9eda100febcbef107f9a2b5fca227c6aeb88 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 7 Jun 2026 15:10:04 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20ValueType=20=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E9=9D=99=E6=80=81=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Display/DisplayBase.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/BootstrapBlazor/Components/Display/DisplayBase.cs b/src/BootstrapBlazor/Components/Display/DisplayBase.cs index 7aa1b44a63e..b0c58f1249a 100644 --- a/src/BootstrapBlazor/Components/Display/DisplayBase.cs +++ b/src/BootstrapBlazor/Components/Display/DisplayBase.cs @@ -47,11 +47,14 @@ public abstract class DisplayBase : BootstrapModuleComponentBase protected bool IsNullable() => NullableUnderlyingType != null; /// - /// 获得/设置 泛型参数 TValue 可为空类型 Type 实例 - /// Gets or sets Generic Parameter TValue Nullable Type Instance + /// 获得 泛型参数 解包后的实际 实例。当 时返回其底层类型,否则返回 自身 + /// Gets the unwrapped of : the underlying type when is , otherwise itself. /// - [NotNull] - protected Type? ValueType { get; set; } + /// + /// 每个封闭泛型类型只计算一次缓存为静态字段,避免热路径重复反射 + /// Cached once per closed generic type to avoid repeated reflection on hot paths. + /// + protected static readonly Type ValueType = NullableUnderlyingType ?? typeof(TValue); /// /// 获得/设置 输入组件的值,支持双向绑定 @@ -135,8 +138,6 @@ public override Task SetParametersAsync(ParameterView parameters) { parameters.SetParameterProperties(this); - ValueType ??= NullableUnderlyingType ?? typeof(TValue); - if (ValueExpression != null) { FieldIdentifier = Microsoft.AspNetCore.Components.Forms.FieldIdentifier.Create(ValueExpression);