diff --git a/src/BootstrapBlazor.Server/Components/Samples/Selects.razor b/src/BootstrapBlazor.Server/Components/Samples/Selects.razor index 7359fb578e5..4b5b6c51e44 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Selects.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/Selects.razor @@ -87,19 +87,6 @@ - -
-
- -
-
- -
-
-
- @@ -348,6 +335,28 @@ + +
+

@((MarkupString)Localizer["SelectsClearableDesc"].Value)

+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ @@ -412,22 +421,23 @@ -
@((MarkupString)Localizer["SelectsVirtualizeDescription"].Value)
- -
-
- - - - -
-
- - - - +
+

@((MarkupString)Localizer["SelectsVirtualizeDescription"].Value)

+
+
+ + + + +
+
+ + + + +
-
+

1. 使用 OnQueryAsync 作为数据源

diff --git a/src/BootstrapBlazor.Server/Components/Samples/Selects.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Selects.razor.cs index ace25321957..11332afc37b 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Selects.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/Selects.razor.cs @@ -51,7 +51,7 @@ public sealed partial class Selects private string? _fooName; - private List _enumValueDemoItems = [ + private readonly List _enumValueDemoItems = [ new("0", "Primary"), new("1", "Middle") ]; @@ -101,7 +101,18 @@ private Task OnItemChanged(SelectedItem item) private Foo BindingModel { get; set; } = new Foo(); - private Foo ClearableModel { get; set; } = new Foo(); + private MockModel ClearableModel { get; set; } = new(); + + class MockModel + { + public string? NullableName { get; set; } + + public string Name { get; set; } = ""; + + public int Count { get; set; } = 1; + + public int? NullableCount { get; set; } + } private SelectedItem? Item { get; set; } @@ -230,6 +241,14 @@ private string GetSelectedBoolItemString() new("abcde", "abcde") ]; + private readonly SelectedItem[] IntItems = + [ + new("1", "1"), + new("12", "12"), + new("123", "123"), + new("1234", "1234") + ]; + private static Task OnBeforeSelectedItemChange(SelectedItem item) { return Task.FromResult(true); diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index b9af6258e8f..3636abe6d2b 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -3111,6 +3111,7 @@ "SelectsBindingIntro": "The values in the text box change as you change the drop-down option by binding the Model.Name property to the component with Select", "SelectsClearableTitle": "Clearable", "SelectsClearableIntro": "You can clear Select using a clear icon", + "SelectsClearableDesc": "Cannot be a null integer. Setting IsClearable has no effect. Its default value is 0", "SelectsBindingSelectedItemTitle": "Select two-way binding SelectItem type", "SelectsBindingSelectedItemIntro": "The values in the text box change as you change the drop-down option by binding the SelectItem property to the component with Select .", "SelectsCascadingTitle": "Select cascading binding", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 9f0918a687f..d1f1067eeca 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -3111,6 +3111,7 @@ "SelectsBindingIntro": "通过 Select 组件绑定 Model.Name 属性,改变下拉框选项时,文本框内的数值随之改变。", "SelectsClearableTitle": "可清空单选", "SelectsClearableIntro": "包含清空按钮,可将选择器清空为初始状态", + "SelectsClearableDesc": "不可为空整形设置 IsClearable 无效,其默认值为 0", "SelectsBindingSelectedItemTitle": "Select 双向绑定 SelectItem", "SelectsBindingSelectedItemIntro": "通过 Select 组件绑定 SelectItem 属性,改变下拉框选项时,文本框内的数值随之改变。", "SelectsCascadingTitle": "Select 级联绑定", diff --git a/src/BootstrapBlazor/Components/Select/Select.razor.cs b/src/BootstrapBlazor/Components/Select/Select.razor.cs index 18766cbd51a..1fb848e459a 100644 --- a/src/BootstrapBlazor/Components/Select/Select.razor.cs +++ b/src/BootstrapBlazor/Components/Select/Select.razor.cs @@ -42,7 +42,7 @@ public partial class Select : ISelect, ILookup .AddClass($"text-danger", IsValid.HasValue && !IsValid.Value) .Build(); - private bool GetClearable() => IsClearable && !IsDisabled; + private bool GetClearable() => IsClearable && !IsDisabled && IsNullable(); /// /// 设置当前项是否 Active 方法 @@ -294,6 +294,11 @@ private SelectedItem? SelectedRow private SelectedItem? GetSelectedRow() { + if (Value is null) + { + return null; + } + var item = GetItemWithEnumValue() ?? Rows.Find(i => i.Value == CurrentValueAsString) ?? Rows.Find(i => i.Active) @@ -393,7 +398,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) /// private int TotalCount { get; set; } - private List GetVirtualItems() => FilterBySearchText(GetRowsByItems()).ToList(); + private List GetVirtualItems() => [.. FilterBySearchText(GetRowsByItems())]; /// /// 虚拟滚动数据加载回调方法 @@ -539,7 +544,6 @@ private async Task SelectedItemChanged(SelectedItem item) { if (_lastSelectedValueString != item.Value) { - item.Active = true; SelectedItem = item; @@ -556,7 +560,7 @@ private async Task SelectedItemChanged(SelectedItem item) } /// - /// 添加静态下拉项方法 + /// /// /// public void Add(SelectedItem item) => _children.Add(item); @@ -577,22 +581,18 @@ private async Task OnClearValue() await OnClearAsync(); } - SelectedItem? item; if (OnQueryAsync != null) { await VirtualizeElement.RefreshDataAsync(); - item = _result.Items.FirstOrDefault(); - } - else - { - item = Items.FirstOrDefault(); - } - if (item != null) - { - await SelectedItemChanged(item); } + + _lastSelectedValueString = string.Empty; + CurrentValue = default; + SelectedItem = null; } + private bool IsNullable() => !ValueType.IsValueType || NullableUnderlyingType != null; + private string? ReadonlyString => IsEditable ? null : "readonly"; private async Task OnChange(ChangeEventArgs args) diff --git a/test/UnitTest/Components/SelectTest.cs b/test/UnitTest/Components/SelectTest.cs index b6ab38eaee4..f1c59c44c6d 100644 --- a/test/UnitTest/Components/SelectTest.cs +++ b/test/UnitTest/Components/SelectTest.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Components.Web.Virtualization; using System.ComponentModel.DataAnnotations; using System.Reflection; +using System.Runtime.CompilerServices; namespace UnitTest.Components; @@ -134,7 +135,7 @@ public void Select_Lookup() } [Fact] - public void IsClearable_Ok() + public async Task IsClearable_Ok() { var val = "Test2"; var cut = Context.RenderComponent>(pb => @@ -154,8 +155,8 @@ public void IsClearable_Ok() }); }); var clearButton = cut.Find(".clear-icon"); - cut.InvokeAsync(() => clearButton.Click()); - Assert.Empty(val); + await cut.InvokeAsync(() => clearButton.Click()); + Assert.Null(val); // 提高代码覆盖率 var select = cut; @@ -174,6 +175,97 @@ public void IsClearable_Ok() validPi.SetValue(select.Instance, false); val = pi.GetValue(select.Instance, null)!.ToString(); Assert.Contains("text-danger", val); + + // 更改数据类型为不可为空 int + // IsClearable 参数无效 + var cut1 = Context.RenderComponent>(pb => + { + pb.Add(a => a.IsClearable, true); + pb.Add(a => a.Items, new List() + { + new("1", "Test1"), + new("2", "Test2"), + new("3", "Test3") + }); + pb.Add(a => a.Value, 1); + }); + cut1.DoesNotContain("clear-icon"); + } + + [Fact] + public void IsNullable_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new List() + { + new("", "请选择"), + new("2", "Test2"), + new("3", "Test3") + }); + }); + Assert.True(IsNullable(cut.Instance)); + + var cut1 = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new List() + { + new("", "请选择"), + new("2", "Test2"), + new("3", "Test3") + }); + }); + Assert.True(IsNullable(cut1.Instance)); + + var cut2 = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new List() + { + new("", "请选择"), + new("2", "Test2"), + new("3", "Test3") + }); + }); + Assert.True(IsNullable(cut2.Instance)); + + var cut3 = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new List() + { + new("", "请选择"), + new("2", "Test2"), + new("3", "Test3") + }); + }); + Assert.True(IsNullable(cut3.Instance)); + + var cut4 = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new List() + { + new("", "请选择"), + new("2", "Test2"), + new("3", "Test3") + }); + }); + Assert.False(IsNullable(cut4.Instance)); + + var cut5 = Context.RenderComponent>(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().GetMethod("IsNullable", BindingFlags.Instance | BindingFlags.NonPublic)!; + return (bool)mi.Invoke(select, null)!; } [Fact] @@ -434,8 +526,7 @@ public void NullBool_Ok() }); // 值为 null - // 候选项中无,导致默认选择第一个 Value 被更改为 true - Assert.True(cut.Instance.Value); + Assert.Null(cut.Instance.Value); } [Fact] @@ -735,8 +826,8 @@ public async Task IsVirtualize_Items_Clearable_Ok() var button = cut.Find(".clear-icon"); await cut.InvokeAsync(() => button.Click()); - // UI 恢复 Test1 - Assert.Equal("Test1", el.Value); + // 可为空数据类型 UI 为 "" + Assert.Equal("", el.Value); // 下拉框显示所有选项 items = cut.FindAll(".dropdown-item"); @@ -794,8 +885,8 @@ public async Task IsVirtualize_OnQueryAsync_Clearable_Ok() var button = cut.Find(".clear-icon"); await cut.InvokeAsync(() => button.Click()); - // UI 恢复 Test1 - Assert.Equal("All", el.Value); + // 可为空数据类型 UI 为 "" + Assert.Equal("", el.Value); // 下拉框显示所有选项 Assert.True(query);