From 3a219b6f159b72a809b17a683f2427837a06778f Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 31 Jan 2025 13:14:33 +0800 Subject: [PATCH] feat(MultiSelect): support Flags attribute (#5253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * doc: 文档格式化 * feat: 支持 Flags 参数 * doc: 增加示例 * doc: 更新示例 * feat: 枚举类型支持 Flags 标签 * test: 增加单元测试 --- .../Components/Samples/MultiSelects.razor | 63 ++++++++++++------- .../Components/Samples/MultiSelects.razor.cs | 23 +++++-- src/BootstrapBlazor.Server/Locales/en-US.json | 2 + src/BootstrapBlazor.Server/Locales/zh-CN.json | 2 + .../Components/Select/MultiSelect.razor.cs | 25 +++++--- .../Extensions/EnumExtensions.cs | 35 +++++++++-- src/BootstrapBlazor/Utils/Utility.cs | 4 ++ test/UnitTest/Components/MultiSelectTest.cs | 27 ++++++++ 8 files changed, 137 insertions(+), 44 deletions(-) diff --git a/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor b/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor index 6b1ed9b4114..b60402b6777 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor @@ -5,31 +5,31 @@

@Localizer["MultiSelectsDescription"]

- +@*
- +
- +
- +
- +
- +
- +
- +
@@ -40,7 +40,7 @@
- +
@@ -51,12 +51,12 @@
- +
- + +
@SelectedItemsValue
@@ -66,12 +66,12 @@
@((MarkupString)Localizer["MultiSelectBindingCollectionDescription"].Value)
- +
- + +
@(string.Join(",", SelectedArrayValues))
@@ -84,9 +84,9 @@
- + +
@(string.Join(",", SelectedIntArrayValues))
@@ -94,11 +94,26 @@
@((MarkupString)Localizer["MultiSelectBindingEnumCollectionDescription"].Value)
- +
@(string.Join(",", SelectedEnumValues))
+
*@ + + +
+
[Flags]
+private enum MultiSelectEnumFoo
+{
+    One = 1,
+    Two = 2,
+    Three = 4,
+    Four = 8
+}
+
+
- +@*
@((MarkupString)Localizer["MultiSelectSearchDescription"].Value)
@SelectedSearchItemsValue
@@ -192,7 +207,7 @@
@((MarkupString)Localizer["MultiSelectCascadingDescription"].Value)
-
@@ -253,7 +268,7 @@
-
+
*@ diff --git a/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor.cs index e7a568e1842..3c0d43256df 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor.cs @@ -62,13 +62,24 @@ public partial class MultiSelects private string SelectedItemsValue { get; set; } = "Beijing"; - private IEnumerable SelectedArrayValues { get; set; } = Enumerable.Empty(); + private IEnumerable SelectedArrayValues { get; set; } = []; private IEnumerable SelectedEnumValues { get; set; } = new List { EnumEducation.Middle, EnumEducation.Primary }; + private MultiSelectEnumFoo EnumFoo { get; set; } = MultiSelectEnumFoo.One | MultiSelectEnumFoo.Two; + + [Flags] + private enum MultiSelectEnumFoo + { + One = 1, + Two = 2, + Three = 4, + Four = 8 + } + [NotNull] private ConsoleLogger? Logger { get; set; } @@ -101,7 +112,7 @@ private async Task OnEditCallback(string value) { await Task.Delay(100); - var item = EditableItems.Find(i => i.Text.Equals(value, System.StringComparison.OrdinalIgnoreCase)); + var item = EditableItems.Find(i => i.Text.Equals(value, StringComparison.OrdinalIgnoreCase)); if (item == null) { item = new SelectedItem(value, value); @@ -120,7 +131,7 @@ private async Task OnEditCallback(string value) new("Ningbo", "宁波") {GroupName = "华东", Active = true } ]; - private readonly SelectedItem[] CascadingItems2 = + private readonly SelectedItem[] _cascadingItems2 = [ new("", "请选择 ..."), new("Beijing", "北京") { Active = true }, @@ -209,12 +220,12 @@ private void AddListItems() private void RemoveListItems() { - SelectedArrayValues = new[] { "Beijing" }; + SelectedArrayValues = ["Beijing"]; } private void ClearListItems() { - SelectedArrayValues = Enumerable.Empty(); + SelectedArrayValues = []; } private void AddArrayItems() @@ -236,7 +247,7 @@ private IEnumerable OnSearch(string searchText) { Logger.Log($"{Localizer["MultiSelectSearchLog"]}:{searchText}"); SearchItemsSource ??= GenerateItems(); - return SearchItemsSource.Where(i => i.Text.Contains(searchText, System.StringComparison.OrdinalIgnoreCase)); + return SearchItemsSource.Where(i => i.Text.Contains(searchText, StringComparison.OrdinalIgnoreCase)); } private Task OnSelectedItemsChanged8(IEnumerable items) diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 10a17becbf1..606ae70c63f 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -2941,6 +2941,8 @@ "MultiSelectSearchTitle": "Search function", "MultiSelectSearchIntro": "Turn on search by setting the ShowSearch value", "MultiSelectSearchDescription": "In this example, the search callback delegate method is set onSearchTextChanged to customize search results if the display text is used internally to make a fuzzy match when not set", + "MultiSelectFlagsEnumTitle": "Flags Enum", + "MultiSelectFlagsEnumIntro": "When the binding value is an Enum data type, if it has a Flags tag, multiple selection mode is automatically supported", "MultiSelectGroupTitle": "Grouping", "MultiSelectGroupIntro": "Alternatives are presented in groups", "MultiSelectDisableTitle": "Disable the feature", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index c7bb88fe88a..597e60ff85f 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -2941,6 +2941,8 @@ "MultiSelectSearchTitle": "搜索功能", "MultiSelectSearchIntro": "通过设置 ShowSearch 值开启搜索功能", "MultiSelectSearchDescription": "本例中设置搜索回调委托方法 OnSearchTextChanged 进行自定义搜索结果,如果未设置时内部使用显示文本进行模糊匹配", + "MultiSelectFlagsEnumTitle": "Flags 枚举", + "MultiSelectFlagsEnumIntro": "绑定值为 Enum 数据类型时,如果枚举有 Flags 标签时,自动支持多选模式", "MultiSelectGroupTitle": "分组", "MultiSelectGroupIntro": "通过设置 GroupName 将下拉框中的备选项进行分组显示", "MultiSelectDisableTitle": "禁用功能", diff --git a/src/BootstrapBlazor/Components/Select/MultiSelect.razor.cs b/src/BootstrapBlazor/Components/Select/MultiSelect.razor.cs index cdea5f0faba..a225524efa9 100644 --- a/src/BootstrapBlazor/Components/Select/MultiSelect.razor.cs +++ b/src/BootstrapBlazor/Components/Select/MultiSelect.razor.cs @@ -5,6 +5,8 @@ using Microsoft.Extensions.Localization; using System.Collections; +using System.Collections.Specialized; +using System.Reflection; namespace BootstrapBlazor.Components; @@ -234,13 +236,15 @@ protected override void OnParametersSet() ResetRules(); _itemsCache = null; + // 通过 Value 对集合进行赋值 - if (PreviousValue != CurrentValueAsString) + var _currentValue = CurrentValueAsString; + if (PreviousValue != _currentValue) { - PreviousValue = CurrentValueAsString; - var list = CurrentValueAsString.Split(',', StringSplitOptions.RemoveEmptyEntries); + PreviousValue = _currentValue; + var list = _currentValue.Split(',', StringSplitOptions.RemoveEmptyEntries); SelectedItems.Clear(); - SelectedItems.AddRange(Rows.Where(item => list.Any(i => i == item.Value))); + SelectedItems.AddRange(Rows.Where(item => list.Any(i => i.Trim() == item.Value))); } } @@ -397,14 +401,13 @@ private void ResetRules() private async Task SetValue() { - var typeValue = NullableUnderlyingType ?? typeof(TValue); - if (typeValue == typeof(string)) + if (ValueType == typeof(string)) { CurrentValueAsString = string.Join(",", SelectedItems.Select(i => i.Value)); } - else if (typeValue.IsGenericType || typeValue.IsArray) + else if (ValueType.IsGenericType || ValueType.IsArray) { - var t = typeValue.IsGenericType ? typeValue.GenericTypeArguments[0] : typeValue.GetElementType()!; + var t = ValueType.IsGenericType ? ValueType.GenericTypeArguments[0] : ValueType.GetElementType()!; var listType = typeof(List<>).MakeGenericType(t); var instance = (IList)Activator.CreateInstance(listType, SelectedItems.Count)!; @@ -415,7 +418,11 @@ private async Task SetValue() instance.Add(val); } } - CurrentValue = (TValue)(typeValue.IsGenericType ? instance : listType.GetMethod("ToArray")!.Invoke(instance, null)!); + CurrentValue = (TValue)(ValueType.IsGenericType ? instance : listType.GetMethod("ToArray")!.Invoke(instance, null)!); + } + else if (ValueType.IsFlagEnum()) + { + CurrentValue = (TValue?)SelectedItems.ParseFlagEnum(ValueType); } if (ValidateForm == null && (Min > 0 || Max > 0)) diff --git a/src/BootstrapBlazor/Extensions/EnumExtensions.cs b/src/BootstrapBlazor/Extensions/EnumExtensions.cs index ffa64d18efb..127f4269a11 100644 --- a/src/BootstrapBlazor/Extensions/EnumExtensions.cs +++ b/src/BootstrapBlazor/Extensions/EnumExtensions.cs @@ -63,11 +63,7 @@ public static List ToSelectList(this Type type, SelectedItem? addi if (type.IsEnum()) { var t = Nullable.GetUnderlyingType(type) ?? type; - foreach (var field in Enum.GetNames(t)) - { - var desc = Utility.GetDisplayName(t, field); - ret.Add(new SelectedItem(field, desc)); - } + ret.AddRange(from field in Enum.GetNames(t) let desc = Utility.GetDisplayName(t, field) select new SelectedItem(field, desc)); } return ret; } @@ -114,4 +110,33 @@ public static bool IsEnum(this Type? type) } return ret; } + + /// + /// 判断类型是否为 Flag 枚举类型 + /// + /// + /// + public static bool IsFlagEnum(this Type? type) => type != null && IsEnum(type) && type.GetCustomAttribute() != null; + + /// + /// 将 集合转换为 Flag 枚举值 + /// + /// + /// + /// + internal static object? ParseFlagEnum(this IEnumerable items, Type type) + { + TValue? v = default; + if (type.IsFlagEnum()) + { + foreach (var item in items) + { + if (Enum.TryParse(type, item.Value, true, out var val)) + { + v = (TValue)Enum.ToObject(type, Convert.ToInt32(v) | Convert.ToInt32(val)); + } + } + } + return v; + } } diff --git a/src/BootstrapBlazor/Utils/Utility.cs b/src/BootstrapBlazor/Utils/Utility.cs index 20d61864124..74f5ff6285c 100644 --- a/src/BootstrapBlazor/Utils/Utility.cs +++ b/src/BootstrapBlazor/Utils/Utility.cs @@ -830,6 +830,10 @@ public static string Format(object? source, IFormatProvider provider) } } } + else if (typeValue.IsFlagEnum()) + { + ret = value!.ToString(); + } return ret; } diff --git a/test/UnitTest/Components/MultiSelectTest.cs b/test/UnitTest/Components/MultiSelectTest.cs index 34b732c0635..6e9e5a8455b 100644 --- a/test/UnitTest/Components/MultiSelectTest.cs +++ b/test/UnitTest/Components/MultiSelectTest.cs @@ -153,6 +153,33 @@ public void EnumValue_Ok() Assert.Contains("multi-select", cut.Markup); } + [Fact] + public async Task FlagEnum_Ok() + { + var value = MockFlagEnum.One | MockFlagEnum.Two; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Value, value); + }); + var values = cut.FindAll(".multi-select-items .multi-select-item"); + Assert.Equal(2, values.Count); + + // 选中第四个 + var item = cut.FindAll(".dropdown-menu .dropdown-item").Last(); + await cut.InvokeAsync(() => item.Click()); + values = cut.FindAll(".multi-select-items .multi-select-item"); + Assert.Equal(3, values.Count); + } + + [Flags] + private enum MockFlagEnum + { + One = 1, + Two = 2, + Three = 4, + Four = 8 + } + [Fact] public void Group_Ok() {