Skip to content

Commit 3a219b6

Browse files
authored
feat(MultiSelect): support Flags attribute (#5253)
* doc: 文档格式化 * feat: 支持 Flags 参数 * doc: 增加示例 * doc: 更新示例 * feat: 枚举类型支持 Flags 标签 * test: 增加单元测试
1 parent ae7a53c commit 3a219b6

File tree

8 files changed

+137
-44
lines changed

8 files changed

+137
-44
lines changed

src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,31 @@
55

66
<h4>@Localizer["MultiSelectsDescription"]</h4>
77

8-
<DemoBlock Title="@Localizer["MultiSelectColorTitle"]" Introduction="@Localizer["MultiSelectColorIntro"]" Name="Color">
8+
@* <DemoBlock Title="@Localizer["MultiSelectColorTitle"]" Introduction="@Localizer["MultiSelectColorIntro"]" Name="Color">
99
<div class="row g-3">
1010
<div class="col-12 col-sm-6">
1111
<MultiSelect TValue="string" Items="@Items1" />
1212
</div>
1313
<div class="col-12 col-sm-6">
14-
<MultiSelect TValue="string" Color="Color.Primary" Items="@Items2" />
14+
<MultiSelect TValue="string" Color="Color.Primary" Items="@Items2"></MultiSelect>
1515
</div>
1616
<div class="col-12 col-sm-6">
17-
<MultiSelect TValue="string" Color="Color.Success" Items="@Items3" />
17+
<MultiSelect TValue="string" Color="Color.Success" Items="@Items3"></MultiSelect>
1818
</div>
1919
<div class="col-12 col-sm-6">
20-
<MultiSelect TValue="string" Color="Color.Danger" Items="@Items4" />
20+
<MultiSelect TValue="string" Color="Color.Danger" Items="@Items4"></MultiSelect>
2121
</div>
2222
<div class="col-12 col-sm-6">
23-
<MultiSelect TValue="string" Color="Color.Warning" Items="@Items5" />
23+
<MultiSelect TValue="string" Color="Color.Warning" Items="@Items5"></MultiSelect>
2424
</div>
2525
<div class="col-12 col-sm-6">
26-
<MultiSelect TValue="string" Color="Color.Info" Items="@Items6" />
26+
<MultiSelect TValue="string" Color="Color.Info" Items="@Items6"></MultiSelect>
2727
</div>
2828
<div class="col-12 col-sm-6">
29-
<MultiSelect TValue="string" Color="Color.Secondary" Items="@Items7" />
29+
<MultiSelect TValue="string" Color="Color.Secondary" Items="@Items7"></MultiSelect>
3030
</div>
3131
<div class="col-12 col-sm-6">
32-
<MultiSelect TValue="string" Color="Color.Dark" Items="@Items8" />
32+
<MultiSelect TValue="string" Color="Color.Dark" Items="@Items8"></MultiSelect>
3333
</div>
3434
</div>
3535
</DemoBlock>
@@ -40,7 +40,7 @@
4040
</section>
4141
<div class="row g-3">
4242
<div class="col-12 col-sm-4">
43-
<MultiSelect TValue="string" Items="@Items1" IsSingleLine="true" />
43+
<MultiSelect TValue="string" Items="@Items1" IsSingleLine="true"></MultiSelect>
4444
</div>
4545
</div>
4646
</DemoBlock>
@@ -51,12 +51,12 @@
5151
</section>
5252
<div class="row g-3">
5353
<div class="col-12 col-sm-6">
54-
<MultiSelect Items="@Items1" @bind-Value="@SelectedItemsValue" />
54+
<MultiSelect Items="@Items1" @bind-Value="@SelectedItemsValue"></MultiSelect>
5555
</div>
5656
<div class="col-12 col-sm-6">
57-
<Button Icon="fa-solid fa-plus" Text="@Localizer["MultiSelectAdd"]" OnClick="@AddItems" class="me-1" />
58-
<Button Icon="fa-solid fa-minus" Text="@Localizer["MultiSelectDecrease"]" OnClick="@RemoveItems" />
59-
<Button Icon="fa-regular fa-trash-can" Text="@Localizer["MultiSelectClean"]" OnClick="@ClearItems" />
57+
<Button Icon="fa-solid fa-plus" Text="@Localizer["MultiSelectAdd"]" OnClick="@AddItems" class="me-1"></Button>
58+
<Button Icon="fa-solid fa-minus" Text="@Localizer["MultiSelectDecrease"]" OnClick="@RemoveItems"></Button>
59+
<Button Icon="fa-regular fa-trash-can" Text="@Localizer["MultiSelectClean"]" OnClick="@ClearItems"></Button>
6060
</div>
6161
</div>
6262
<section ignore>@SelectedItemsValue</section>
@@ -66,12 +66,12 @@
6666
<section ignore>@((MarkupString)Localizer["MultiSelectBindingCollectionDescription"].Value)</section>
6767
<div class="row g-3">
6868
<div class="col-12 col-sm-6">
69-
<MultiSelect Items="@Items" @bind-Value="@SelectedArrayValues" Max="4" Min="2" />
69+
<MultiSelect Items="@Items" @bind-Value="@SelectedArrayValues" Max="4" Min="2"></MultiSelect>
7070
</div>
7171
<div class="col-12 col-sm-6">
72-
<Button Icon="fa-solid fa-plus" Text="@Localizer["MultiSelectAdd"]" OnClick="@AddListItems" class="me-1" />
73-
<Button Icon="fa-solid fa-minus" Text="@Localizer["MultiSelectDecrease"]" OnClick="@RemoveListItems" />
74-
<Button Icon="fa-regular fa-trash-can" Text="@Localizer["MultiSelectClean"]" OnClick="@ClearListItems" />
72+
<Button Icon="fa-solid fa-plus" Text="@Localizer["MultiSelectAdd"]" OnClick="@AddListItems" class="me-1"></Button>
73+
<Button Icon="fa-solid fa-minus" Text="@Localizer["MultiSelectDecrease"]" OnClick="@RemoveListItems"></Button>
74+
<Button Icon="fa-regular fa-trash-can" Text="@Localizer["MultiSelectClean"]" OnClick="@ClearListItems"></Button>
7575
</div>
7676
</div>
7777
<section ignore>@(string.Join(",", SelectedArrayValues))</section>
@@ -84,21 +84,36 @@
8484
<MultiSelect Items="@LongItems" @bind-Value="@SelectedIntArrayValues" />
8585
</div>
8686
<div class="col-12 col-sm-6">
87-
<Button Icon="fa-solid fa-plus" Text="@Localizer["MultiSelectAdd"]" OnClick="@AddArrayItems" class="me-1" />
88-
<Button Icon="fa-solid fa-minus" Text="@Localizer["MultiSelectDecrease"]" OnClick="@RemoveArrayItems" />
89-
<Button Icon="fa-regular fa-trash-can" Text="@Localizer["MultiSelectClean"]" OnClick="@ClearArrayItems" />
87+
<Button Icon="fa-solid fa-plus" Text="@Localizer["MultiSelectAdd"]" OnClick="@AddArrayItems" class="me-1"></Button>
88+
<Button Icon="fa-solid fa-minus" Text="@Localizer["MultiSelectDecrease"]" OnClick="@RemoveArrayItems"></Button>
89+
<Button Icon="fa-regular fa-trash-can" Text="@Localizer["MultiSelectClean"]" OnClick="@ClearArrayItems"></Button>
9090
</div>
9191
</div>
9292
<section ignore>@(string.Join(",", SelectedIntArrayValues))</section>
9393
</DemoBlock>
9494
9595
<DemoBlock Title="@Localizer["MultiSelectBindingEnumCollectionTitle"]" Introduction="@Localizer["MultiSelectBindingEnumCollectionIntro"]" Name="BindingEnumCollection">
9696
<section ignore>@((MarkupString)Localizer["MultiSelectBindingEnumCollectionDescription"].Value)</section>
97-
<MultiSelect @bind-Value="@SelectedEnumValues" />
97+
<MultiSelect @bind-Value="@SelectedEnumValues"></MultiSelect>
9898
<section ignore>@(string.Join(",", SelectedEnumValues))</section>
99+
</DemoBlock> *@
100+
101+
<DemoBlock Title="@Localizer["MultiSelectFlagsEnumTitle"]" Introduction="@Localizer["MultiSelectFlagsEnumIntro"]"
102+
Name="Flags">
103+
<section ignore>
104+
<Pre>[Flags]
105+
private enum MultiSelectEnumFoo
106+
{
107+
One = 1,
108+
Two = 2,
109+
Three = 4,
110+
Four = 8
111+
}</Pre>
112+
</section>
113+
<MultiSelect @bind-Value="@EnumFoo"></MultiSelect>
99114
</DemoBlock>
100115

101-
<DemoBlock Title="@Localizer["MultiSelectSearchTitle"]" Introduction="@Localizer["MultiSelectSearchIntro"]" Name="Search">
116+
@* <DemoBlock Title="@Localizer["MultiSelectSearchTitle"]" Introduction="@Localizer["MultiSelectSearchIntro"]" Name="Search">
102117
<section ignore>@((MarkupString)Localizer["MultiSelectSearchDescription"].Value)</section>
103118
<MultiSelect Items="@Items" @bind-Value="@SelectedSearchItemsValue" ShowSearch="true" OnSearchTextChanged="@OnSearch" />
104119
<section ignore>@SelectedSearchItemsValue</section>
@@ -192,7 +207,7 @@
192207
<section ignore>@((MarkupString)Localizer["MultiSelectCascadingDescription"].Value)</section>
193208
<div class="row g-3">
194209
<div class="col-12 col-sm-6">
195-
<Select TValue="string" Items="@CascadingItems2" OnSelectedItemChanged="@OnCascadeBindSelectClick" />
210+
<Select TValue="string" Items="@_cascadingItems2" OnSelectedItemChanged="@OnCascadeBindSelectClick"></Select>
196211
</div>
197212
<div class="col-12 col-sm-6">
198213
<MultiSelect TValue="string" Items="@CascadingItems1" />
@@ -253,7 +268,7 @@
253268
<Display Value="@_editString"></Display>
254269
</div>
255270
</div>
256-
</DemoBlock>
271+
</DemoBlock> *@
257272

258273
<AttributeTable Items="@GetAttributes()" />
259274

src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,24 @@ public partial class MultiSelects
6262

6363
private string SelectedItemsValue { get; set; } = "Beijing";
6464

65-
private IEnumerable<string> SelectedArrayValues { get; set; } = Enumerable.Empty<string>();
65+
private IEnumerable<string> SelectedArrayValues { get; set; } = [];
6666

6767
private IEnumerable<EnumEducation> SelectedEnumValues { get; set; } = new List<EnumEducation>
6868
{
6969
EnumEducation.Middle, EnumEducation.Primary
7070
};
7171

72+
private MultiSelectEnumFoo EnumFoo { get; set; } = MultiSelectEnumFoo.One | MultiSelectEnumFoo.Two;
73+
74+
[Flags]
75+
private enum MultiSelectEnumFoo
76+
{
77+
One = 1,
78+
Two = 2,
79+
Three = 4,
80+
Four = 8
81+
}
82+
7283
[NotNull]
7384
private ConsoleLogger? Logger { get; set; }
7485

@@ -101,7 +112,7 @@ private async Task<SelectedItem> OnEditCallback(string value)
101112
{
102113
await Task.Delay(100);
103114

104-
var item = EditableItems.Find(i => i.Text.Equals(value, System.StringComparison.OrdinalIgnoreCase));
115+
var item = EditableItems.Find(i => i.Text.Equals(value, StringComparison.OrdinalIgnoreCase));
105116
if (item == null)
106117
{
107118
item = new SelectedItem(value, value);
@@ -120,7 +131,7 @@ private async Task<SelectedItem> OnEditCallback(string value)
120131
new("Ningbo", "宁波") {GroupName = "华东", Active = true }
121132
];
122133

123-
private readonly SelectedItem[] CascadingItems2 =
134+
private readonly SelectedItem[] _cascadingItems2 =
124135
[
125136
new("", "请选择 ..."),
126137
new("Beijing", "北京") { Active = true },
@@ -209,12 +220,12 @@ private void AddListItems()
209220

210221
private void RemoveListItems()
211222
{
212-
SelectedArrayValues = new[] { "Beijing" };
223+
SelectedArrayValues = ["Beijing"];
213224
}
214225

215226
private void ClearListItems()
216227
{
217-
SelectedArrayValues = Enumerable.Empty<string>();
228+
SelectedArrayValues = [];
218229
}
219230

220231
private void AddArrayItems()
@@ -236,7 +247,7 @@ private IEnumerable<SelectedItem> OnSearch(string searchText)
236247
{
237248
Logger.Log($"{Localizer["MultiSelectSearchLog"]}{searchText}");
238249
SearchItemsSource ??= GenerateItems();
239-
return SearchItemsSource.Where(i => i.Text.Contains(searchText, System.StringComparison.OrdinalIgnoreCase));
250+
return SearchItemsSource.Where(i => i.Text.Contains(searchText, StringComparison.OrdinalIgnoreCase));
240251
}
241252

242253
private Task OnSelectedItemsChanged8(IEnumerable<SelectedItem> items)

src/BootstrapBlazor.Server/Locales/en-US.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2941,6 +2941,8 @@
29412941
"MultiSelectSearchTitle": "Search function",
29422942
"MultiSelectSearchIntro": "Turn on search by setting the <code>ShowSearch</code> value",
29432943
"MultiSelectSearchDescription": "In this example, the search callback delegate method is set <code>onSearchTextChanged</code> to customize search results if the display text is used internally to make a fuzzy match when not set",
2944+
"MultiSelectFlagsEnumTitle": "Flags Enum",
2945+
"MultiSelectFlagsEnumIntro": "When the binding value is an <code>Enum</code> data type, if it has a <code>Flags</code> tag, multiple selection mode is automatically supported",
29442946
"MultiSelectGroupTitle": "Grouping",
29452947
"MultiSelectGroupIntro": "Alternatives are presented in groups",
29462948
"MultiSelectDisableTitle": "Disable the feature",

src/BootstrapBlazor.Server/Locales/zh-CN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2941,6 +2941,8 @@
29412941
"MultiSelectSearchTitle": "搜索功能",
29422942
"MultiSelectSearchIntro": "通过设置 <code>ShowSearch</code> 值开启搜索功能",
29432943
"MultiSelectSearchDescription": "本例中设置搜索回调委托方法 <code>OnSearchTextChanged</code> 进行自定义搜索结果,如果未设置时内部使用显示文本进行模糊匹配",
2944+
"MultiSelectFlagsEnumTitle": "Flags 枚举",
2945+
"MultiSelectFlagsEnumIntro": "绑定值为 <code>Enum</code> 数据类型时,如果枚举有 <code>Flags</code> 标签时,自动支持多选模式",
29442946
"MultiSelectGroupTitle": "分组",
29452947
"MultiSelectGroupIntro": "通过设置 <code>GroupName</code> 将下拉框中的备选项进行分组显示",
29462948
"MultiSelectDisableTitle": "禁用功能",

src/BootstrapBlazor/Components/Select/MultiSelect.razor.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
using Microsoft.Extensions.Localization;
77
using System.Collections;
8+
using System.Collections.Specialized;
9+
using System.Reflection;
810

911
namespace BootstrapBlazor.Components;
1012

@@ -234,13 +236,15 @@ protected override void OnParametersSet()
234236
ResetRules();
235237

236238
_itemsCache = null;
239+
237240
// 通过 Value 对集合进行赋值
238-
if (PreviousValue != CurrentValueAsString)
241+
var _currentValue = CurrentValueAsString;
242+
if (PreviousValue != _currentValue)
239243
{
240-
PreviousValue = CurrentValueAsString;
241-
var list = CurrentValueAsString.Split(',', StringSplitOptions.RemoveEmptyEntries);
244+
PreviousValue = _currentValue;
245+
var list = _currentValue.Split(',', StringSplitOptions.RemoveEmptyEntries);
242246
SelectedItems.Clear();
243-
SelectedItems.AddRange(Rows.Where(item => list.Any(i => i == item.Value)));
247+
SelectedItems.AddRange(Rows.Where(item => list.Any(i => i.Trim() == item.Value)));
244248
}
245249
}
246250

@@ -397,14 +401,13 @@ private void ResetRules()
397401

398402
private async Task SetValue()
399403
{
400-
var typeValue = NullableUnderlyingType ?? typeof(TValue);
401-
if (typeValue == typeof(string))
404+
if (ValueType == typeof(string))
402405
{
403406
CurrentValueAsString = string.Join(",", SelectedItems.Select(i => i.Value));
404407
}
405-
else if (typeValue.IsGenericType || typeValue.IsArray)
408+
else if (ValueType.IsGenericType || ValueType.IsArray)
406409
{
407-
var t = typeValue.IsGenericType ? typeValue.GenericTypeArguments[0] : typeValue.GetElementType()!;
410+
var t = ValueType.IsGenericType ? ValueType.GenericTypeArguments[0] : ValueType.GetElementType()!;
408411
var listType = typeof(List<>).MakeGenericType(t);
409412
var instance = (IList)Activator.CreateInstance(listType, SelectedItems.Count)!;
410413

@@ -415,7 +418,11 @@ private async Task SetValue()
415418
instance.Add(val);
416419
}
417420
}
418-
CurrentValue = (TValue)(typeValue.IsGenericType ? instance : listType.GetMethod("ToArray")!.Invoke(instance, null)!);
421+
CurrentValue = (TValue)(ValueType.IsGenericType ? instance : listType.GetMethod("ToArray")!.Invoke(instance, null)!);
422+
}
423+
else if (ValueType.IsFlagEnum())
424+
{
425+
CurrentValue = (TValue?)SelectedItems.ParseFlagEnum<TValue>(ValueType);
419426
}
420427

421428
if (ValidateForm == null && (Min > 0 || Max > 0))

src/BootstrapBlazor/Extensions/EnumExtensions.cs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,7 @@ public static List<SelectedItem> ToSelectList(this Type type, SelectedItem? addi
6363
if (type.IsEnum())
6464
{
6565
var t = Nullable.GetUnderlyingType(type) ?? type;
66-
foreach (var field in Enum.GetNames(t))
67-
{
68-
var desc = Utility.GetDisplayName(t, field);
69-
ret.Add(new SelectedItem(field, desc));
70-
}
66+
ret.AddRange(from field in Enum.GetNames(t) let desc = Utility.GetDisplayName(t, field) select new SelectedItem(field, desc));
7167
}
7268
return ret;
7369
}
@@ -114,4 +110,33 @@ public static bool IsEnum(this Type? type)
114110
}
115111
return ret;
116112
}
113+
114+
/// <summary>
115+
/// 判断类型是否为 Flag 枚举类型
116+
/// </summary>
117+
/// <param name="type"></param>
118+
/// <returns></returns>
119+
public static bool IsFlagEnum(this Type? type) => type != null && IsEnum(type) && type.GetCustomAttribute<FlagsAttribute>() != null;
120+
121+
/// <summary>
122+
/// 将 <see cref="IEnumerable{T}"/> 集合转换为 Flag 枚举值
123+
/// </summary>
124+
/// <param name="items"></param>
125+
/// <param name="type"></param>
126+
/// <returns></returns>
127+
internal static object? ParseFlagEnum<TValue>(this IEnumerable<SelectedItem> items, Type type)
128+
{
129+
TValue? v = default;
130+
if (type.IsFlagEnum())
131+
{
132+
foreach (var item in items)
133+
{
134+
if (Enum.TryParse(type, item.Value, true, out var val))
135+
{
136+
v = (TValue)Enum.ToObject(type, Convert.ToInt32(v) | Convert.ToInt32(val));
137+
}
138+
}
139+
}
140+
return v;
141+
}
117142
}

src/BootstrapBlazor/Utils/Utility.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,10 @@ public static string Format(object? source, IFormatProvider provider)
830830
}
831831
}
832832
}
833+
else if (typeValue.IsFlagEnum())
834+
{
835+
ret = value!.ToString();
836+
}
833837
return ret;
834838
}
835839

test/UnitTest/Components/MultiSelectTest.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,33 @@ public void EnumValue_Ok()
153153
Assert.Contains("multi-select", cut.Markup);
154154
}
155155

156+
[Fact]
157+
public async Task FlagEnum_Ok()
158+
{
159+
var value = MockFlagEnum.One | MockFlagEnum.Two;
160+
var cut = Context.RenderComponent<MultiSelect<MockFlagEnum>>(pb =>
161+
{
162+
pb.Add(a => a.Value, value);
163+
});
164+
var values = cut.FindAll(".multi-select-items .multi-select-item");
165+
Assert.Equal(2, values.Count);
166+
167+
// 选中第四个
168+
var item = cut.FindAll(".dropdown-menu .dropdown-item").Last();
169+
await cut.InvokeAsync(() => item.Click());
170+
values = cut.FindAll(".multi-select-items .multi-select-item");
171+
Assert.Equal(3, values.Count);
172+
}
173+
174+
[Flags]
175+
private enum MockFlagEnum
176+
{
177+
One = 1,
178+
Two = 2,
179+
Three = 4,
180+
Four = 8
181+
}
182+
156183
[Fact]
157184
public void Group_Ok()
158185
{

0 commit comments

Comments
 (0)