Files

279 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Select 选择器
## 概述
下拉选择器(Select)是数据录入场景中最常见的表单控件之一,用于在大量选项中选择一个或多个值。它通过弹出下拉面板呈现可选项列表,配合搜索过滤、远程加载、分组等高级功能,满足从简单枚举到复杂数据源的各种选择需求。
AtomUI 的 `Select` 控件完整复刻了 [Ant Design 5.0 Select](https://ant.design/components/select-cn) 的设计规范,在 .NET / Avalonia 平台上提供与 Ant Design React 版一致的视觉体验和交互行为。
---
## 设计原理
### Ant Design 的 Select 设计哲学
Ant Design 对 Select 的定位是:**「弹出一个下拉菜单给用户选择操作,用于代替原生的选择器,或者需要一个更优雅的多选器时」**。其核心设计准则:
**三种选择模式**(满足不同业务场景):
| 模式 | 设计意图 | 典型用途 |
|---|---|---|
| 🔘 **单选(Single)** | 从选项列表中选择唯一值,选中后下拉关闭 | 城市选择、状态筛选、分类选择 |
| ☑️ **多选(Multiple)** | 从选项列表中选择多个值,以标签(Tag)形式展示已选项 | 标签选择、权限分配、多条件筛选 |
| 🏷️ **标签(Tags)** | 在多选基础上允许用户自由输入创建新标签 | 文章标签、自定义分类 |
**三种样式变体**(对齐输入控件家族):
| 变体 | 设计意图 |
|---|---|
| **Outlined** | 默认外观,有明确的边框轮廓,适合大多数表单场景 |
| **Filled** | 填充背景,适合需要视觉层次感的场景 |
| **Borderless** | 无边框,适合嵌入表格或其他容器中的轻量选择 |
**核心交互特性**:
- **搜索过滤**:展开时可通过输入关键字过滤选项
- **清空操作**:可选的清除按钮一键清空已选值
- **异步加载**:支持从远程数据源动态加载选项
- **分组展示**:选项支持按分类分组,配合分组标题
- **响应式标签**:多选模式下标签自动折叠,超出显示计数
- **自定义渲染**:通过模板自定义下拉选项的展示方式
### Avalonia 基础能力
AtomUI 的 `Select` 并非继承自 Avalonia 内置的 `ComboBox`,而是基于 `TemplatedControl` 全新实现。这一设计决策的原因:
1. Ant Design Select 的功能远超传统 ComboBox(多选、标签、搜索过滤、异步加载等)
2. 需要对 Popup、选项列表、标签面板等进行完全定制化的控制
3. 与 AtomUI 的 Design Token 系统和 AddOnDecoratedBox 装饰器体系深度集成
**继承链:**
```
Control → TemplatedControl → AbstractSelect → Select
```
### AtomUI 的扩展设计
AtomUI `Select` 基于 `TemplatedControl` 从零构建,实现了以下能力:
| 扩展能力 | 实现方式 | 设计动机 |
|---|---|---|
| **三种选择模式** | `SelectMode` 枚举(Single / Multiple / Tags) | 对齐 Ant Design 的 `mode` 属性 |
| **三种样式变体** | `IInputControlStyleVariantAware` 接口 + `StyleVariant` | 对齐 Ant Design 的 `variant`,支持 Outlined / Filled / Borderless |
| **三种尺寸** | `ISizeTypeAware` 接口 + `SizeType` 共享属性 | 全局统一的 Large / Middle / Small 尺寸系统 |
| **搜索过滤** | `IsFilterEnabled` + `Filter` + `FilterValueSelector` | 展开时输入关键字实时过滤选项 |
| **异步加载** | `OptionsLoader` + `ISelectOptionsAsyncLoader` 接口 | 支持从远程数据源异步加载选项 |
| **分组展示** | `IsGroupEnabled` + `GroupPropertySelector` | 选项按分组标题聚合展示 |
| **清空操作** | `IsAllowClear` + `SelectHandle` 清除按钮 | 一键清空已选值 |
| **响应式标签** | `IsResponsiveTagMode` + `MaxTagCount` | 多选标签自动折叠,超出显示 `+N` 计数 |
| **最大选择数** | `MaxCount` + `IsShowMaxCountIndicator` | 限制最大可选数量,超出后选项禁用 |
| **下拉弹出** | 内置 `Popup` + 动画支持 | 下拉面板支持动画打开/关闭 |
| **AddOn 装饰** | 继承 `AddOnDecoratedBox` 属性 | 支持前/后附加内容区域 |
| **表单集成** | `IFormItemAware` + `IFormItemFeedbackAware` 接口 | 参与 FormItem 验证流程,支持 Error/Warning 状态 |
| **紧凑空间** | `ICompactSpaceAware` 接口 | 在 `Space.Compact` 容器中自动调整圆角和边框 |
| **Design Token** | `SelectToken` + `RegisterTokenResourceScope` | 所有视觉值从全局/组件级 Token 派生,支持主题切换 |
---
## 功能详解
### 选择模式(SelectMode)
选择模式通过 `Mode` 属性设置:
| 模式 | 行为 | 已选展示 |
|---|---|---|
| `Single` | 单选,选中后自动关闭下拉。支持搜索输入框替代已选文本 | 直接显示已选项的 `Header` 文本 |
| `Multiple` | 多选,选中后不关闭下拉。每个已选项以 Tag 形式展示 | `SelectTag` 标签列表 + 可选的搜索输入框 |
| `Tags` | 在多选基础上,输入内容无匹配时自动创建新选项 | 同 `Multiple`,但支持动态添加选项 |
**Tags 模式的特殊行为**:
- 搜索功能强制启用(`IsEffectiveFilterEnabled = true`)
- 当搜索无匹配结果时,自动创建临时 `SelectOption`(`IsDynamicAdded = true`)
- 选中后动态选项保留;取消选中时自动从选项列表移除
### 搜索过滤
当 `IsFilterEnabled = true`(或 `Mode = Tags`)时启用搜索过滤:
- **Single 模式**:展开时显示输入框,输入关键字过滤选项
- **Multiple/Tags 模式**:标签区域内嵌输入框,支持边选边搜
过滤机制:
- 默认按 `Header` 属性匹配(`HeaderFilterPropertySelector`)
- 可通过 `FilterValueSelector` 自定义匹配字段
- 可通过 `Filter` 属性提供自定义匹配逻辑(`IValueFilter` 实现)
### 异步加载
通过 `OptionsLoader` 属性绑定 `ISelectOptionsAsyncLoader` 实现:
1. 首次点击打开时触发 `LoadAsync`
2. 加载期间显示 Loading 图标(`SuffixLoadingIcon`)
3. 加载完成后自动填充选项并展开下拉
4. 支持通过 `OptionsAsyncLoadContext` 传递加载上下文
5. 提供 `OptionsLoading` / `OptionsLoaded` 事件监听加载过程
### 分组展示
当 `IsGroupEnabled = true` 时启用分组:
- 选项通过 `Group` 属性(`IGroupListItemData`)进行分组
- 可通过 `GroupPropertySelector` 自定义分组属性提取逻辑
- 每个分组显示分组标题
### 响应式标签
多选模式下处理大量已选项的展示:
| 属性 | 功能 |
|---|---|
| `MaxTagCount` | 固定最多显示 N 个标签,超出部分折叠为 `+N` 计数 |
| `IsResponsiveTagMode` | 根据控件实际宽度自动计算可见标签数量 |
| `MaxTagPlaceholder` | 自定义折叠后的显示文本 |
### 下拉弹出控制
| 属性 | 功能 |
|---|---|
| `IsDropDownOpen` | 控制下拉是否打开 |
| `IsDefaultOpen` | 初始时是否默认打开 |
| `Placement` | 下拉面板弹出位置(`BottomEdgeAlignedLeft` / `TopEdgeAlignedLeft` 等) |
| `IsPopupMatchSelectWidth` | 下拉面板是否与 Select 控件等宽(默认 `true`) |
| `DisplayPageSize` | 下拉面板最多显示的选项条数(默认 10,控制最大高度) |
### 键盘交互
| 按键 | 行为 |
|---|---|
| `F4` / `Alt+Down` / `Alt+Up` | 切换下拉展开/关闭 |
| `Down` / `Up`(下拉关闭时) | 打开下拉 |
| `Escape`(下拉打开时) | 关闭下拉 |
| `Enter` / `Space`(下拉关闭时) | 打开下拉 |
| `Backspace` / `Delete`(多选模式) | 删除最后一个已选标签 |
---
## 与 Ant Design 规范的对齐程度
| 特性 | Ant Design (React) | AtomUI (Avalonia) | 对齐情况 |
|---|---|---|---|
| 三种模式 `mode` | ✅ `/ multiple / tags` | ✅ `SelectMode` 枚举 | ✅ 完全对齐 |
| 三种变体 `variant` | ✅ `outlined / filled / borderless` | ✅ `StyleVariant` 属性 | ✅ 完全对齐 |
| 三种尺寸 `size` | ✅ `large / middle / small` | ✅ `SizeType` 属性 | ✅ 完全对齐 |
| 搜索过滤 `showSearch` | ✅ 布尔属性 | ✅ `IsFilterEnabled` | ✅ 完全对齐 |
| 自定义过滤 `filterOption` | ✅ 函数 | ✅ `Filter`(`IValueFilter`) | ✅ 对齐(类型不同,语义一致) |
| 允许清空 `allowClear` | ✅ 布尔/对象 | ✅ `IsAllowClear` 布尔 | ✅ 完全对齐 |
| 远程加载 | ✅ `filterOption={false}` | ✅ `OptionsLoader` + `ISelectOptionsAsyncLoader` | ✅ 对齐(API 不同,能力一致) |
| 最大选择数 `maxCount` | ✅ 数值 | ✅ `MaxCount` 属性 | ✅ 完全对齐 |
| 响应式标签 `maxTagCount` | ✅ `responsive` / 数值 | ✅ `IsResponsiveTagMode` / `MaxTagCount` | ✅ 完全对齐 |
| 分组 `OptGroup` | ✅ 组件 | ✅ `IsGroupEnabled` + `Group` 属性 | ✅ 对齐(声明式 → 属性驱动) |
| 自定义渲染 `optionRender` | ✅ 函数 | ✅ `OptionTemplate`(`IDataTemplate`) | ✅ 对齐(模板化方式) |
| 弹出位置 `placement` | ✅ `bottomLeft` 等 | ✅ `Placement` 枚举 | ✅ 完全对齐 |
| 占位文本 `placeholder` | ✅ 字符串 | ✅ `PlaceholderText` | ✅ 完全对齐 |
| 默认值 `defaultValue` | ✅ 任意类型 | ✅ `DefaultValues` 属性 | ✅ 完全对齐 |
| 前/后附加 `prefix` / `suffixIcon` | ✅ 自定义内容 | ✅ `ContentLeftAddOn` / `SuffixIcon` | ✅ 完全对齐 |
| 隐藏已选 | ✅ 通过 `filterOption` 实现 | ✅ `IsHideSelectedOptions` | ✅ 完全对齐 |
| 状态 `status` | ✅ `error / warning` | ✅ `Status` 属性 | ✅ 完全对齐 |
| 下拉宽度匹配 `popupMatchSelectWidth` | ✅ 布尔/数值 | ✅ `IsPopupMatchSelectWidth` | ⚠️ 仅支持布尔,不支持自定义宽度值 |
| `labelInValue` | ✅ 返回 label 和 value | ❌ 暂未支持 | — 通过 `ISelectOption` 可获取全部信息 |
| `virtual` 虚拟滚动 | ✅ 默认开启 | ✅ 通过 `VirtualizingStackPanel` 实现 | ✅ 完全对齐 |
| 空状态 `notFoundContent` | ✅ 自定义内容 | ✅ `EmptyIndicator` / `EmptyIndicatorTemplate` | ✅ 完全对齐 |
---
## 继承关系
```
Avalonia.Controls.Primitives.TemplatedControl
└── AtomUI.Desktop.Controls.AbstractSelect
├── implements IMotionAwareControl
├── implements ISizeTypeAware
├── implements ICompactSpaceAware
├── implements IInputControlStatusAware
├── implements IInputControlStyleVariantAware
├── implements IFormItemAware
└── implements IFormItemFeedbackAware
└── AtomUI.Desktop.Controls.Select
└── 注册 SelectToken.ScopeProvider
```
> 注意:`AbstractSelect` 和 `Select` 均定义在 `AtomUI.Desktop.Controls` 命名空间下(位于 `src/AtomUI.Desktop.Controls/Select/` 目录)。当前 Select 不存在跨平台基类层(`AtomUI.Controls` 中无 AbstractSelect),所有逻辑均在桌面控件层实现。
**各层级职责划分:**
| 层级 | 提供的能力 |
|---|---|
| `TemplatedControl` | 模板化控件基础设施、`OnApplyTemplate`、样式系统 |
| `AbstractSelect` | 下拉选择器通用基础设施:Popup 管理(打开/关闭/动画)、下拉展开/折叠事件、占位文本、输入框样式变体(Outlined/Filled/Borderless)、验证状态(Error/Warning)、AddOn 装饰器属性、紧凑空间支持、表单集成、清除按钮逻辑、弹出位置管理 |
| `Select` | 具体选择行为实现:三种选择模式(Single/Multiple/Tags)、选项数据管理(`Options` / `OptionsSource`)、选中值管理(`SelectedOption` / `SelectedOptions`)、搜索过滤、异步加载、分组展示、标签管理、默认值处理、键盘交互 |
**实现的共享接口:**
| 接口 | 定义位置 | 作用 |
|---|---|---|
| `ISizeTypeAware` | `AtomUI.Controls.Shared` | 支持 `SizeType`(Small / Middle / Large)三种尺寸切换 |
| `IMotionAwareControl` | `AtomUI.Controls.Shared` | 支持 `IsMotionEnabled` 动画开关 |
| `ICompactSpaceAware` | `AtomUI.Controls.Shared` | 在 `Space.Compact` 中使用时自动调整圆角和边框 |
| `IInputControlStatusAware` | `AtomUI.Controls.Shared` | 支持 `Status`(Error / Warning)验证状态 |
| `IInputControlStyleVariantAware` | `AtomUI.Controls.Shared` | 支持 `StyleVariant`(Outlined / Filled / Borderless)样式变体 |
| `IFormItemAware` | `AtomUI.Controls.Shared` | 可作为 `FormItem` 的子控件参与表单验证 |
| `IFormItemFeedbackAware` | `AtomUI.Controls.Shared` | 接收表单验证反馈控件 |
---
## 源码位置
| 层级 | 文件路径 | 说明 |
|---|---|---|
| 基类 | `src/AtomUI.Desktop.Controls/Select/AbstractSelect.cs` | 下拉选择器通用基础设施 |
| 控件类 | `src/AtomUI.Desktop.Controls/Select/Select.cs` | Select 主控件类 |
| 异步加载 | `src/AtomUI.Desktop.Controls/Select/Select.AsyncOptionsLoad.cs` | 异步选项加载逻辑(partial class) |
| 选项接口 | `src/AtomUI.Desktop.Controls/Select/SelectOption.cs` | `ISelectOption` 接口 + `SelectOption` 记录类 |
| 伪类常量 | `src/AtomUI.Desktop.Controls/Select/SelectPseudoClass.cs` | 伪类定义 |
| 弹出位置枚举 | `src/AtomUI.Desktop.Controls/Select/SelectPopupPlacement.cs` | 弹出位置枚举 |
| 选择变更事件 | `src/AtomUI.Desktop.Controls/Select/SelectSelectionChangedEventArgs.cs` | 选择变更事件参数 |
| 异步加载接口 | `src/AtomUI.Desktop.Controls/Select/DataLoad/ISelectOptionsAsyncLoader.cs` | 异步加载器接口 |
| 加载结果 | `src/AtomUI.Desktop.Controls/Select/DataLoad/SelectOptionsLoadResult.cs` | 加载结果类 |
| Token 定义 | `src/AtomUI.Desktop.Controls/Select/SelectToken.cs` | 组件级 Design Token |
| 主题模板 | `src/AtomUI.Desktop.Controls/Select/Themes/SelectTheme.axaml` | ControlTheme AXAML |
| 模板常量 | `src/AtomUI.Desktop.Controls/Select/Themes/SelectThemeConstants.cs` | 模板部件名称常量 |
| Gallery 示例 | `controlgallery/AtomUIGallery/ShowCases/Views/DataEntry/SelectShowCase.axaml` | 使用范例 |
---
## 模板结构
Select 的 ControlTemplate 采用 Panel 布局,将选择器区域和弹出下拉面板分离:
```
Panel
├── SelectAddOnDecoratedBox (PART_AddOnDecoratedBox) ← 输入框装饰容器(边框、AddOn 区域)
│ ├── [ContentRightAddOn] ← 右侧附加区域
│ │ ├── SelectMaxCountIndicator ← 最大选择数指示器(可选显示)
│ │ ├── ContentPresenter (ContentRightAddOn) ← 用户自定义右侧内容
│ │ └── SelectHandle ← 下拉箭头/清除/加载图标区域
│ └── Panel ← 内容主区域
│ ├── TextBlock#PlaceholderText ← 占位文本
│ ├── ContentPresenter#SingleSelectResultPresenter ← 单选结果展示(仅 Single 模式可见)
│ ├── SelectFilterTextBox (PART_SingleFilterInput) ← 单选搜索输入框(仅 Single+Filter 可见)
│ └── SelectResultOptionsBox#SelectedOptionsBox ← 多选标签+搜索区域(仅 Multiple/Tags 可见)
└── Popup (PART_Popup) ← 下拉弹出面板
└── Border#PopupFrame ← 弹出框架(背景、圆角、阴影)
└── SelectCandidateList (PART_CandidateList) ← 候选选项列表(虚拟滚动)
```
**分层设计理由:**
- **AddOnDecoratedBox 装饰**:统一输入控件的边框、AddOn 和样式变体系统,与 Input、AutoComplete 等控件保持一致的外观风格
- **SelectHandle 独立**:集中管理下拉箭头、清除按钮、加载图标和表单反馈控件的切换逻辑
- **弹出面板独立**:Popup 使用 OverlayLayer,不影响主控件布局
### 模板部件常量
| 常量名 | 值 | 说明 |
|---|---|---|
| `SelectThemeConstants.CandidateListPart` | `"PART_CandidateList"` | 候选选项列表 |
| `SelectThemeConstants.PopupPart` | `"PART_Popup"` | 下拉弹出面板 |
| `SelectThemeConstants.SingleFilterInputPart` | `"PART_SingleFilterInput"` | 单选搜索输入框 |
| `SelectHandleThemeConstants.ClearButtonPart` | `"PART_ClearButton"` | 清除按钮 |