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
4 changes: 1 addition & 3 deletions src/BootstrapBlazor/Components/AutoFill/AutoFill.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Avoid duplicating null-assignability logic across components

Both AutoFill and SelectBase now inline (!ValueType.IsValueType || IsNullable()). Since this is effectively "can TValue be cleared to null", consider a shared helper on the base class (e.g. protected bool IsNullAssignable() => !ValueType.IsValueType || IsNullable();) to remove duplication and keep future nullability changes centralized.

Suggested implementation:

    private bool GetClearable() => IsClearable && !IsDisabled && IsNullAssignable();

To fully implement the suggestion and remove duplication:

  1. Add a shared helper on the common base class for AutoFill and SelectBase (e.g. wherever ValueType and IsNullable() are currently defined):
protected bool IsNullAssignable() => !ValueType.IsValueType || IsNullable();
  1. Update SelectBase (or any other components that currently inline (!ValueType.IsValueType || IsNullable())) to call IsNullAssignable() instead of duplicating the logic.

  2. Ensure IsNullable() remains implemented in the base class (or in a place accessible to both AutoFill and SelectBase) so that IsNullAssignable() compiles correctly.


private async Task OnClickItem(TValue val)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ protected override void OnInitialized()
base.OnInitialized();

// 泛型设置为可为空
AllowNull = NullableUnderlyingType != null;
AllowNull = IsNullable();
}

/// <summary>
Expand Down
23 changes: 17 additions & 6 deletions src/BootstrapBlazor/Components/Display/DisplayBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,24 @@ public abstract class DisplayBase<TValue> : BootstrapModuleComponentBase
protected static readonly Type? NullableUnderlyingType = Nullable.GetUnderlyingType(typeof(TValue));

/// <summary>
/// <para lang="zh">获得/设置 泛型参数 TValue 可为空类型 Type 实例</para>
/// <para lang="en">Gets or sets Generic Parameter TValue Nullable Type Instance</para>
/// <para lang="zh">判断泛型参数 <typeparamref name="TValue"/> 是否为 <see cref="Nullable{T}"/> 可空值类型</para>
/// <para lang="en">Determines whether the generic parameter <typeparamref name="TValue"/> is a <see cref="Nullable{T}"/> value type.</para>
/// </summary>
[NotNull]
protected Type? ValueType { get; set; }
/// <returns>
/// <para lang="zh"><see langword="true"/> 表示 <typeparamref name="TValue"/> 为 <see cref="Nullable{T}"/> 否则返回 <see langword="false"/></para>
/// <para lang="en"><see langword="true"/> when <typeparamref name="TValue"/> is <see cref="Nullable{T}"/>; otherwise <see langword="false"/>.</para>
/// </returns>
protected bool IsNullable() => NullableUnderlyingType != null;

/// <summary>
/// <para lang="zh">获得 泛型参数 <typeparamref name="TValue"/> 解包后的实际 <see cref="Type"/> 实例。当 <typeparamref name="TValue"/> 为 <see cref="Nullable{T}"/> 时返回其底层类型,否则返回 <typeparamref name="TValue"/> 自身</para>
/// <para lang="en">Gets the unwrapped <see cref="Type"/> of <typeparamref name="TValue"/>: the underlying type when <typeparamref name="TValue"/> is <see cref="Nullable{T}"/>, otherwise <typeparamref name="TValue"/> itself.</para>
/// </summary>
/// <remarks>
/// <para lang="zh">每个封闭泛型类型只计算一次缓存为静态字段,避免热路径重复反射</para>
/// <para lang="en">Cached once per closed generic type to avoid repeated reflection on hot paths.</para>
/// </remarks>
protected static readonly Type ValueType = NullableUnderlyingType ?? typeof(TValue);

/// <summary>
/// <para lang="zh">获得/设置 输入组件的值,支持双向绑定</para>
Expand Down Expand Up @@ -125,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/Components/Radio/RadioList.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ protected override void OnParametersSet()
var t = ValueType;
if (t.IsEnum && Items == null)
{
Items = t.ToSelectList<TValue>((NullableUnderlyingType != null && IsAutoAddNullItem) ? new SelectedItem<TValue>(default!, NullItemText) : null);
Items = t.ToSelectList<TValue>((IsNullable() && IsAutoAddNullItem) ? new SelectedItem<TValue>(default!, NullItemText) : null);
}

Items ??= [];
Expand Down
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/Components/Select/Select.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
4 changes: 1 addition & 3 deletions src/BootstrapBlazor/Components/Select/SelectBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,11 @@ protected override void OnParametersSet()
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public Task Hide() => InvokeVoidAsync("hide", Id);

private bool IsNullable() => !ValueType.IsValueType || NullableUnderlyingType != null;

/// <summary>
/// <para lang="zh">获得 是否显示清除按钮</para>
/// <para lang="en">Gets whether show the clear button</para>
/// </summary>
protected bool GetClearable() => IsClearable && !IsDisabled && IsNullable();
protected bool GetClearable() => IsClearable && !IsDisabled && (!ValueType.IsValueType || IsNullable());

/// <summary>
/// <para lang="zh">清除搜索文本</para>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TValue>(string.IsNullOrEmpty(item) ? null : new SelectedItem<TValue>(default!, item));
}

Expand Down
4 changes: 2 additions & 2 deletions src/BootstrapBlazor/Components/Validate/ValidateBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -498,7 +498,7 @@ private void ValidateType(ValidationContext context, List<ValidationResult> resu
{
// 增加数据基础类型验证 如泛型约定为 int 文本框值为 Empty
// 可为空泛型约束时不检查
if (NullableUnderlyingType == null && PreviousParsingAttemptFailed)
if (!IsNullable() && PreviousParsingAttemptFailed)
{
var memberNames = new string[] { context.MemberName! };
results.Add(new ValidationResult(PreviousErrorMessage, memberNames));
Expand Down
76 changes: 0 additions & 76 deletions test/UnitTest/Components/SelectTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,82 +229,6 @@ public async Task IsClearable_Ok()
cut1.DoesNotContain("clear-icon");
}

[Fact]
public void IsNullable_Ok()
{
var cut = Context.Render<Select<string>>(pb =>
{
pb.Add(a => a.Items, new List<SelectedItem>()
{
new("", "请选择"),
new("2", "Test2"),
new("3", "Test3")
});
});
Assert.True(IsNullable(cut.Instance));

var cut1 = Context.Render<Select<string?>>(pb =>
{
pb.Add(a => a.Items, new List<SelectedItem>()
{
new("", "请选择"),
new("2", "Test2"),
new("3", "Test3")
});
});
Assert.True(IsNullable(cut1.Instance));

var cut2 = Context.Render<Select<Foo>>(pb =>
{
pb.Add(a => a.Items, new List<SelectedItem>()
{
new("", "请选择"),
new("2", "Test2"),
new("3", "Test3")
});
});
Assert.True(IsNullable(cut2.Instance));

var cut3 = Context.Render<Select<Foo?>>(pb =>
{
pb.Add(a => a.Items, new List<SelectedItem>()
{
new("", "请选择"),
new("2", "Test2"),
new("3", "Test3")
});
});
Assert.True(IsNullable(cut3.Instance));

var cut4 = Context.Render<Select<int>>(pb =>
{
pb.Add(a => a.Items, new List<SelectedItem>()
{
new("", "请选择"),
new("2", "Test2"),
new("3", "Test3")
});
});
Assert.False(IsNullable(cut4.Instance));

var cut5 = Context.Render<Select<int?>>(pb =>
{
pb.Add(a => a.Items, new List<SelectedItem>()
{
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()
{
Expand Down
Loading