- 前端
- UI组件
- 设计系统
【免费下载链接】ant-design-blazor
基于 Ant Design 与 Blazor 的前端组件库。让开发者解放生产力,实现更大价值。
导读
本文以 ant-design-blazor 官方Select组件「分组」(Option Group)示例文档为核心,系统讲解如何利用GroupName参数将下拉选项按数据对象的某个属性聚合为多个分组,并配合SortByLabel/SortByGroup实现组内与组间排序。读者将掌握:基于DataSource+ 反射属性名(nameof)驱动分组的完整写法、分组渲染与排序的源码原理(SortedSelectOptionItems、SelectOptionGroup),以及为什么启用分组后必须排序才能保证键盘上下键导航正确。
一、什么是 Select 分组(Option Group)
当选项数量较多、且选项天然具有类别归属时(例如员工按"Manager / Engineer"分类),把所有条目平铺在同一个下拉列表中会让用户难以快速定位。ant-design-blazor 的Select组件支持通过**分组指示符(group indicator)**将条目聚合成组,组名以独立的标题行显示在下拉列表中,视觉上呈现为"组标题 + 组成员"的结构。
分组的核心是GroupName参数,其官方说明为:
用作组指示符的属性的名称。如果设置了该值,则条目将按组显示。使用额外的
SortByGroup和SortByLabel。
该定义同时出现在官方 API 文档 Select 参数表 以及源码参数声明 Select.razor.cs 中:
/// <summary> /// The name of the property to be used as a group indicator. /// If the value is set, the entries are displayed in groups. /// Use additional <see cref="SelectBase{TItemValue, TItem}.SortByGroup"/> and <see cref="SelectBase{TItemValue, TItem}.SortByLabel"/>. /// </summary> [Parameter] public string GroupName { get => _groupName; set { _getGroup = string.IsNullOrWhiteSpace(value) ? null : PathHelper.GetDelegate<TItem, string>(value); _groupName = value; } }注意:GroupName的 setter 使用PathHelper.GetDelegate<TItem, string>将属性名字符串编译为访问委托,所以传值方式是属性名的字符串(示例中使用nameof(Person.Role)),而不是属性值本身;如果传空字符串或 null,分组功能即被关闭(_getGroup为 null)。
二、示例文档解读与完整可运行代码
本主题对应的官方演示文档为 optgroup.md,其核心说明为:
条目可以使用组指示符进行分组,通过参数
GroupName实现。使用GroupName参数时建议对条目进行排序(SortByLabel | SortByGroup),否则键盘导航可能出现问题。
与之配套的完整示例代码位于 Optgroup.razor:
<Select TItem="Person" TItemValue="string" DataSource="@_persons" @bind-Value="@_selectedValue" ValueName="@nameof(Person.Value)" LabelName="@nameof(Person.Name)" GroupName="@nameof(Person.Role)" SortByLabel="SortDirection.Ascending" SortByGroup="SortDirection.Ascending" OnSelectedItemChanged="OnSelectedItemChangedHandler" DefaultActiveFirstOption="true" Style="width: 200px;"> </Select> <br /><br /> <p> Selected Value: @_selectedValue <br/> Selected Item Name: @_selectedItem?.Name </p> @code { class Person { public string Value { get; set; } public string Name { get; set; } public string Role { get; set; } } List<Person> _persons; string _selectedValue; Person _selectedItem; protected override void OnInitialized() { _persons = new List<Person> { new Person {Value = "jack", Name = "Jack", Role = "Manager"}, new Person {Value = "lucy", Name = "Lucy", Role = "Manager"}, new Person {Value = "yaoming", Name = "Yaoming", Role = "Engineer"} }; } private void OnSelectedItemChangedHandler(Person value) { _selectedItem = value; Console.WriteLine($"selected: ${value?.Name}"); } }关键参数逐一说明
| 参数 | 示例值 | 作用 |
|---|---|---|
TItem/TItemValue | Person/string | 数据项类型与值类型 |
DataSource | _persons | 选项数据源(IEnumerable<TItem>),见 Select.razor.cs |
@bind-Value | _selectedValue | 选中值的双向绑定 |
ValueName | nameof(Person.Value) | 从数据项中提取"值"的属性名,与ValueProperty二选一 |
LabelName | nameof(Person.Name) | 从数据项中提取"标签"的属性名,与LabelProperty二选一 |
GroupName | nameof(Person.Role) | 分组依据的属性名,本示例按角色(Manager / Engineer)分组 |
SortByLabel | SortDirection.Ascending | 组内按标签升序排序 |
SortByGroup | SortDirection.Ascending | 组间按组名升序排序 |
DefaultActiveFirstOption | true | 打开下拉时默认高亮第一个未禁用的选项 |
OnSelectedItemChanged | 回调方法 | 选中项变化时返回完整的TItem对象(区别于只返回TItemValue的ValueChanged) |
SortDirection枚举定义于 SortDirection.cs,可取值为None、Ascending、Descending;SortByGroup与SortByLabel的默认值均为SortDirection.None(见 SelectBase.razor.cs)。
三、分组与排序的源码实现原理
3.1 分组渲染入口:IsGroupingEnabled 与 SelectOptionGroup
在 Select.razor 的下拉渲染逻辑中,组件会先判断分组开关:
@if (!IsGroupingEnabled) { @SelectOptionsRender() } else { <CascadingValue Value="@ItemTemplate" Name="ItemTemplate"> <SelectOptionGroup TItemValue="TItemValue" TItem="TItem"></SelectOptionGroup> </CascadingValue> }IsGroupingEnabled的定义为!string.IsNullOrWhiteSpace(GroupName)(见 Select.razor.cs),即只要GroupName非空,下拉列表就走分组渲染分支。
分组标题本身由内部组件SelectOptionGroup渲染,其模板位于 SelectOptionGroup.razor:
@foreach (var selectOption in SelectParent.SortedSelectOptionItems) { if (_oldGroupName == selectOption.GroupName) { // 与上一项同组:只渲染选项 <CascadingValue Value="@selectOption.InternalId" Name="InternalId"> @selectOptionFragment(selectOption) </CascadingValue> } else { // 组名发生变化:先输出分组标题行,再渲染选项 if(SelectParent.SelectOptionItems.Any(i=>i.GroupName==selectOption.GroupName && !i.IsHidden)) { <div class="@ClassMapper.Class">@selectOption.GroupName</div> } <CascadingValue Value="@selectOption.InternalId" Name="InternalId"> @selectOptionFragment(selectOption) </CascadingValue> _oldGroupName = selectOption.GroupName; } }渲染逻辑的核心是基于已排序的列表做"相邻项组名比较":当遍历到的选项组名与上一个不同时,先输出一个<div>组标题(CSS 类ant-select-item-group,见 SelectOptionGroup.razor.cs),再渲染属于新组的选项。因此,如果选项列表未按组名排好序,同一个组会被拆成多段、重复输出多个同名的组标题。
3.2 排序组合矩阵:SortedSelectOptionItems
分组渲染与键盘导航共用的有序列表是SortedSelectOptionItems,其实现位于 SelectBase.razor.cs。源码完整枚举了SortByGroup×SortByLabel的 9 种组合(None分支直接返回原始列表):
| SortByGroup | SortByLabel | 实际排序 |
|---|---|---|
None | None | 保持DataSource原始顺序 |
Ascending | None | OrderBy(GroupName) |
Descending | None | OrderByDescending(GroupName) |
None | Ascending | OrderBy(Label) |
None | Descending | OrderByDescending(Label) |
Ascending | Ascending | OrderBy(GroupName).ThenBy(Label) |
Ascending | Descending | OrderBy(GroupName)后对 Label 降序 |
Descending | Ascending | OrderByDescending(GroupName).ThenBy(Label) |
Descending | Descending | OrderByDescending(GroupName)后对 Label 降序 |
其中Label来自数据项的LabelName属性,GroupName来自数据项的GroupName属性;这两个值在CreateDeleteSelectOptions构建选项模型时被写入每个SelectOptionItem(见 Select.razor.cs):
if (!string.IsNullOrWhiteSpace(GroupName)) groupName = _getGroup(item); ... var newItem = new SelectOptionItem<TItemValue, TItem> { Label = label, GroupName = groupName, ... };SelectOptionItem.GroupName是分组信息的最终载体(见 SelectOptionItem.cs),它会同步到对应的SelectOption子组件上。
3.3 组内选项的缩进样式
分组模式下,组内选项会额外带ant-select-item-option-grouped类(见 SelectOption.razor.cs 中SetClassMap的.If($"{ClassPrefix}-grouped", ...)),样式表中为其设置了padding-left: @control-padding-horizontal * 2的缩进(见 index.less),使组成员在视觉上明显内缩于组标题之下;组标题本身则是次要文本色、小号字体且不可点击(index.less)。右到左(RTL)场景下的对应样式见 rtl.less。
四、为什么启用分组后必须排序?
官方文档特别强调:使用GroupName参数时建议对条目排序(SortByLabel | SortByGroup),否则键盘导航可能出问题。这可以从源码得到两方面印证:
组标题的去重依赖排序:如 3.1 节所述,
SelectOptionGroup通过"相邻项组名是否变化"来决定是否插入组标题行。若组名交错出现(如 Manager、Engineer、Manager),同名组会被拆成多个标题行,列表结构混乱。键盘导航依赖有序列表:
Select的上下键导航(OnKeyUpAsync中的ARROWUP/ARROWDOWN分支)全部基于SortedSelectOptionItems计算"下一个可激活选项"的索引(见 Select.razor.cs)。例如下移逻辑会执行:
var sortedSelectOptionItems = SortedSelectOptionItems.ToList(); ... var index = sortedSelectOptionItems.FindIndex(x => EqualityComparer<TItemValue>.Default.Equals(x.Value, firstActive.Value)); index++; var nextIndex = sortedSelectOptionItems.FindIndex(index, x => !x.IsHidden && !x.IsDisabled);这里的索引计算完全依赖列表的顺序性。如果启用分组却不排序,SortedSelectOptionItems返回的是原始顺序,而组标题行又是按"已排序视图"插入的,二者一旦不一致,激活项的"前后邻居"定位就可能落到错误的选项上,导致上下键跳跃异常。而SortByLabel/SortByGroup会让SortedSelectOptionItems变成一份顺序稳定、组内相邻的视图,既保证组标题正确合并,也保证方向键按"组 → 组内标签"的稳定序列导航。
五、进阶与组合使用建议
- 与搜索/过滤共存:分组不影响搜索过滤。
FilterOptionItems只通过IsHidden隐藏不匹配项(见 Select.razor.cs),分组标题的显示还额外检查!i.IsHidden(见 SelectOptionGroup.razor),即组内全部被过滤掉时不会输出空组标题。 - 多选/标签模式下同样生效:分组逻辑与
Mode无关,GroupName+ 排序在多选(multiple)、标签(tags)模式下同样按组展示,只是选中项以 Tag 形式呈现在选择框内,下拉分组结构不变。 - 运行时动态更新分组:若希望分组随数据变化(例如角色字段被修改),需要关注
IgnoreItemChanges参数。它默认true以提升性能;当该参数为false时,CreateDeleteSelectOptions会同步更新已存在选项的GroupName等字段(见 Select.razor.cs)。 - 定位分组字段:分组依据建议使用数据对象中稳定、枚举取值有限的字段(如
Role、Category),避免使用高基数字段导致每个选项独占一组、失去分组意义。
六、相关资源索引
- 分组示例文档:optgroup.md
- 分组示例源码:Optgroup.razor
- Select 完整 API 表(zh-CN):index.zh-CN.md
- Select API 表(en-US):index.en-US.md
GroupName参数声明与分组开关:Select.razor.cs 与 Select.razor.cs- 排序组合实现:
SortedSelectOptionItems(SelectBase.razor.cs) - 分组渲染模板:SelectOptionGroup.razor 与 SelectOptionGroup.razor.cs
- 选项数据模型(承载
GroupName):SelectOptionItem.cs - 键盘导航实现:Select.razor.cs
- 分组样式:index.less
SortDirection枚举:SortDirection.cs
结语
ant-design-blazor 的 Select 分组能力由GroupName一个参数开启,但要让分组正确、可导航、可搜索,必须遵循"设置GroupName的同时显式声明SortByLabel/SortByGroup"这一组合约定。本文从官方示例出发,结合 Select.razor.cs 与 SelectBase.razor.cs 的排序、渲染与键盘导航源码,解释了这一约定背后的技术必然性,帮助你在实际项目中写出分组清晰、键盘体验完善的 Select。
- 前端
- UI组件
- 设计系统
【免费下载链接】ant-design-blazor
基于 Ant Design 与 Blazor 的前端组件库。让开发者解放生产力,实现更大价值。
相关推荐
amlogic-s9xxx-armbian 项目支持 DG-TN3568:从空白刷机到 SATA 盘被识别
amlogic s9xxx armbian 项目支持 DG TN3568:从空白刷机到 SATA 盘被识别 DG TN3568(RK3568)已被 amlogi
UI组件前端ant-design-blazor 列表选择器(Table Select)实战:用 Select 自定义下拉模板集成表格选择
ant design blazor 列表选择器(Table Select)实战:用 Select 自定义下拉模板集成表格选择 导读 本篇文章聚焦 ant des
前端UI组件设计系统ant-design-blazor 下拉选择器 Select 组件完全指南:API 详解、数据源绑定与多选/标签模式实战
ant design blazor 下拉选择器 Select 组件完全指南:API 详解、数据源绑定与多选/标签模式实战 本文以 ant design blaz
前端UI组件设计系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考