# AtomUI 过滤体系架构与使用指南
> 适用于 `AtomUI.Desktop.Controls` 下所有支持"在一组候选项里按输入筛选"的控件:AutoComplete / ComboBox / Mentions / Select / Cascader / TreeSelect / TreeView / Transfer / ListBox / ListView。
## 1. 设计目标
AtomUI 把"过滤"抽象成统一体系,面向这几个诉求:
- **一套契约覆盖所有控件**:过滤谓词、过滤值、选择器三件套完全同构
- **样式层零侵入**:`IsFilterEnabled` 作为"用户意图开关"由 ControlTheme 消费,不做隐式派生
- **层级控件不妥协**:Cascader / TreeSelect / TreeView 的父链、路径、高亮策略这些真正有差异的能力原状保留
- **不要抽象化过度**:统一的是"谓词 + 值选择",其它行为差异交给各控件自己处理
## 2. 核心契约
过滤能力由三件东西构成。
### 2.1 `IValueFilter` —— 过滤谓词
```csharp
namespace AtomUI.Controls.Utils;
public interface IValueFilter
{
ValueFilterMode Mode => ValueFilterMode.Custom;
bool Filter(object? value, object? filterValue);
}
```
- `value` 是从数据项中抽出的可比较值(字符串、数值、任意类型都可以)
- `filterValue` 是当前过滤条件(通常是搜索框文本)
- 返回 `true` 表示命中
`Mode` 属性让某些控件(比如 AutoComplete)能基于过滤类型做分支行为(前缀匹配时的最优候选选择),所以框架内置实现都老老实实填写了。
### 2.2 `DefaultFilterValueSelector` —— 值选择器
```csharp
public delegate object? DefaultFilterValueSelector(object? value);
```
职责:把数据项(item 原始对象)转成可以丢给 `IValueFilter.Filter` 比对的那个值。
- 对纯字符串列表,selector 直接返回自身
- 对 `ISelectOption`、`ICascaderItemInfo`、`TreeViewItem` 这类结构化数据,selector 负责抽 `Header`、`Content`、`Path` 等
- 对复杂业务对象,selector 可以是用户自定义 lambda,返回最有意义的那个字段或者拼接后的字符串
参数类型是 `object?`,自定义 selector 的 lambda 内部需要做空检查。
### 2.3 `ValueFilterMode` + `ValueFilterFactory` —— 内置谓词
```csharp
public enum ValueFilterMode
{
None, Custom,
StartsWith, StartsWithCaseSensitive, StartsWithOrdinal, StartsWithOrdinalCaseSensitive,
Contains, ContainsCaseSensitive, ContainsOrdinal, ContainsOrdinalCaseSensitive,
Equals, EqualsCaseSensitive, EqualsOrdinal, EqualsOrdinalCaseSensitive,
}
public static class ValueFilterFactory
{
public static IValueFilter? BuildFilter(ValueFilterMode mode);
}
```
三种匹配策略(StartsWith / Contains / Equals)× 四种比较规则(文化敏感/Ordinal × 大小写敏感/不敏感),共 12 个内置 `IValueFilter`,绝大多数搜索场景都不需要写自定义实现。
## 3. 控件 API 总览
全部支持过滤的控件对外暴露的过滤相关属性命名一致:
| 属性 | 类型 | 作用 |
|---|---|---|
| `Filter` | `IValueFilter?` | 过滤谓词。`null` 代表不过滤;控件通常在 `OnInitialized` 里给默认值(一般是 `Contains`) |
| `FilterValue` | `object?` / `string?` | 当前过滤条件。通常由控件内部搜索框通过 `SetCurrentValue` 写入,也可由外部绑定 |
| `FilterValueSelector` | `DefaultFilterValueSelector?` | 值选择器。`null` 时控件用自己的默认 selector |
| `IsFilterEnabled` | `bool` | 用户意图开关。是否显示搜索 UI / 启用过滤路径 |
层级 / 高亮相关属性(只有部分控件有):
| 属性 | 出现在 | 作用 |
|---|---|---|
| `FilterStrategy` | TreeSelect、TreeView | 树过滤策略(匹配部分高亮、展开路径、隐藏未匹配、显示整棵树等) |
| `FilterHighlightStrategy` | Cascader、ListBox、ListView | 文本高亮策略(全部、匹配部分、隐藏未匹配等) |
| `FilterHighlightForeground` | Cascader、TreeSelect、TreeView、ListBox、ListView | 高亮颜色 |
| `FilterResultCount` | Cascader、TreeView、ListBox | 过滤后命中的条目数 |
| `IsFiltering` | ListView、ListBox | "当前正在过滤中"的通知信号 |
| `SourceFilterValue` / `TargetFilterValue` | Transfer | 左右两栏独立的过滤条件(`FilterValue` 不存在于 Transfer) |
Transfer 的特殊性:它是双栏结构,过滤条件左右独立,但 `Filter` 和 `FilterValueSelector` 两栏共用。
ComboBox 的特殊性:过滤只在 `IsEditable=True && IsFilterEnabled=True` 时实际生效。用户输入写入 `Text`,控件内部再同步为 `FilterValue`,弹出层使用独立候选视图展示过滤结果;原始 `Items` / `ItemsSource` 不会被替换或裁剪,选择语义仍回写到原 ComboBox 项。
## 4. 数据流
所有控件共用这条路径:
```
[用户输入搜索文本]
│
▼
FilterValue ─────► 触发 OnPropertyChanged
│ │
│ ▼
│ for each item in Items:
│ object? selectedValue
│ = FilterValueSelector?.Invoke(item) ?? 控件内置默认抽值
│ if Filter.Filter(selectedValue, FilterValue) → 保留 / 高亮
│ else → 隐藏(依 HighlightStrategy 而定)
│
▼
IsFilterEnabled ControlTheme 依此切换搜索框可见性 / 布局
```
关键点:
- **Filter 调用永远是两元**(`value`, `filterValue`),不带 owner、不带 item 对象本身
- **层级信息由 selector 负责编码**:例如 Cascader 默认 selector 取 `ICascaderItemInfo.Path`,里头已经是 `"华东 / 上海"` 这样的全路径字符串,谓词只看字符串就够
- **IsFilterEnabled 和 Filter 没有派生关系**:用户设 `IsFilterEnabled=True` 表示"我要过滤 UI",默认谓词 `Filter` 只在控件初始化时 fallback 到 `Contains`;两者解耦
## 5. 使用示例
### 5.1 最常见场景:可搜索的 Select
```xml
```
不需要写任何 C#。`IsFilterEnabled=True` 打开搜索框,`Filter` 默认用 `Contains`,selector 默认取 `ISelectOption.Header`。
### 5.2 可编辑并过滤的 ComboBox
```xml
```
ComboBox 复用 Avalonia 自身的 `IsEditable` / `Text` 契约。过滤候选项来自当前 `Items`,不会改写 `ItemsSource`,所以外部绑定集合、选中项和原始索引语义保持稳定。
### 5.3 自定义匹配策略
```xml
```
或者在 code-behind:
```csharp
myAutoComplete.Filter = ValueFilterFactory.BuildFilter(ValueFilterMode.StartsWithCaseSensitive);
```
### 5.4 自定义值选择器
场景:ListView 绑定了复杂业务对象,希望按多个字段拼接后匹配。
```csharp
myListView.FilterValueSelector = record =>
{
if (record is User user)
{
return $"{user.Name} {user.Department} {user.Email}";
}
return record?.ToString();
};
```
`record` 是 `object?`,务必做空检查。
### 5.5 完全自定义谓词
当内置 12 种匹配策略都不满足(比如要做模糊拼音匹配、正则、或者跨字段加权):
```csharp
public sealed class PinyinFilter : IValueFilter
{
public ValueFilterMode Mode => ValueFilterMode.Custom;
public bool Filter(object? value, object? filterValue)
{
var text = value?.ToString();
var kw = filterValue?.ToString();
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(kw)) return false;
return PinyinHelper.Contains(text, kw);
}
}
// 使用
myCascader.Filter = new PinyinFilter();
```
### 5.6 Transfer 双栏过滤
```xml
```
`SourceFilterValue` / `TargetFilterValue` 是 internal 的,由 ControlTheme 内部搜索框驱动;外部不需要手工绑它们。
## 6. 各控件的默认 Selector
控件内置的默认 selector 在 `OnInitialized` 里自动装上。用户可以随时覆盖。
| 控件 | 默认 selector 行为 |
|---|---|
| AutoComplete | 取 `IAutoCompleteOption.Value` / 字符串项原样 |
| ComboBox | 优先取 `TextSearch.Text`;其次取 `TextSearch.TextBinding` / `DisplayMemberBinding` 计算值;`ComboBoxItem` 取 `Content`;最后 `item.ToString()` |
| Mentions | 取 `IMentionOption.Content` |
| Select | `HeaderFilterPropertySelector` 或 `ValueFilterPropertySelector`(取决于 `FilterBy` 配置) |
| Cascader | `(value) => (value as ICascaderItemInfo)?.Path`,`Path` 在 `CascaderView` 内部沿 `ICascaderOption.ParentNode` 预先拼好,例如 `"华东 / 上海"` |
| TreeView / TreeSelect | `(value) => (value as TreeViewItem)?.Header?.ToString()`,能看到 `Header` 为 `ITreeItemNode` 时走 `Header.Header` |
| ListBox | 按 `IListItemData.Content` 或 string header 取 |
| ListView | 随列定义,没有硬编码默认 |
| Transfer | 没有默认 selector,依赖用户提供或直接 `item.ToString()` |
## 7. IsFilterEnabled 的定位
`IsFilterEnabled` 在这套体系里是**用户意图开关**,不是"Filter 非空的派生信号"。这条语义决定了几件事:
1. **ControlTheme 直接消费**:`^[IsFilterEnabled=True]` 伪类 selector 和 `TemplateBinding IsFilterEnabled` 在 axaml 里正常使用,不需要绑到别的派生属性
2. **Select 的 `IsEffectiveFilterEnabled`** 是 internal DirectProperty,Select 在 Tags 模式下强制为 true,非 Tags 模式下等于 `IsFilterEnabled`;它是 Select 内部对"实际生效状态"的二次派生,不对外
3. **ComboBox 的 `IsEffectiveFilterEnabled`** 是 internal DirectProperty,只有 `IsEditable && IsFilterEnabled` 时为 true;它用于区分"显示可编辑输入"和"弹出候选列表是否过滤"
4. **Filter == null 不会自动关闭搜索框**:`IsFilterEnabled=True` + `Filter=null` 的组合合法,行为是搜索框显示但过滤谓词被控件回落到默认 `Contains`
5. **Filter 非空也不会自动打开搜索框**:只设 `Filter` 而不设 `IsFilterEnabled=True` 时,搜索 UI 不显示,过滤路径不走
这是有意为之的解耦:样式开关与谓词供应是正交的两件事。
## 8. 何时该写什么
- **只是想开一个搜索框**:`IsFilterEnabled="True"`,不碰 `Filter`
- **ComboBox 想输入时过滤候选项**:同时设置 `IsEditable="True"` 和 `IsFilterEnabled="True"`
- **想换匹配策略**:设 `Filter = ValueFilterFactory.BuildFilter(...)`
- **想按复合字段过滤**:设 `FilterValueSelector = record => ...`
- **想完全自定义匹配**:实现 `IValueFilter`,设到 `Filter`
- **想要树过滤策略**:TreeView / TreeSelect 设 `FilterStrategy`
- **想要文本高亮策略**:Cascader / ListBox / ListView 设 `FilterHighlightStrategy`
- **想让自己写的新控件也加入这套体系**:参见下节
## 9. 新控件接入指引
如果你在 `AtomUI.Desktop.Controls` 里开发新的可过滤控件,遵循以下模板:
### 9.1 属性定义
```csharp
public static readonly StyledProperty FilterProperty =
AvaloniaProperty.Register(nameof(Filter));
public static readonly StyledProperty