14 KiB
TreeSelect 桌面版实现原理
本文档描述 TreeSelect 桌面版的输入壳体、树数据集合、选择同步、过滤、候选弹层、Form 和 Token 资源边界。共享输入分层见 输入控件共享架构设计,公共设计与 API 契约见 TreeSelect 桌面版架构设计,Token 语义见 TreeSelect Token 设计,变化记录见 TreeSelect Changelog。
Popup 接入边界:AbstractSelect 负责业务状态和内容准备,PART_Popup 负责实际显示。模板重建或宿主切换时必须先释放旧 relay,再绑定新的 Popup;普通外点、Escape、失焦和业务关闭在 pinned 状态下被拦截,detach、窗口销毁、跨 TopLevel 和无效锚点必须走生命周期关闭并释放 Popup host。完整状态机见 Popup 钉住打开设计。
1. 实现定位
TreeSelect 的实现以 AbstractSelect 为选择与弹层基类,输入表面复用 InputControlFrame / AddOnDecoratedBox,TreeSelect 本体负责树数据、树选择、过滤输入和结果展示协调。树节点容器、展开、勾选和异步加载由 TreeView 家族承担。本文档聚焦 TreeSelect 自身的状态编排、生命周期和维护边界,不重新说明 TreeView 的通用容器回放、shared frame 的输入外观或 PopupHost 的全局资源规则。
2. 源码文件结构
主要源码:
src/AtomUI.Desktop.Controls/TreeSelect/TreeSelect.cs:public TreeSelect API、生命周期、弹层树懒创建、选择同步、过滤输入、Form 映射和私有算法。src/AtomUI.Desktop.Controls/TreeSelect/TreeSelectAddOnDecoratedBox.cs:TreeSelect 输入布局扩展,复用InputControlFrame并承载多选和选择空状态。src/AtomUI.Desktop.Controls/TreeSelect/TreeSelectTreeView.cs:TreeSelect 候选树,使用 TreeView 样式键并创建专用容器。src/AtomUI.Desktop.Controls/TreeSelect/TreeViewSelectTreeViewItem.cs:候选树节点容器,承接最大选择数状态。src/AtomUI.Desktop.Controls/TreeSelect/Converters/*:树节点显示转换辅助。src/AtomUI.Desktop.Controls/TreeSelect/TreeSelectToken.cs:TreeSelect 控件 Token。src/AtomUI.Desktop.Controls/TreeSelect/Themes/TreeSelectTheme.axaml:根模板、输入壳体、右侧内容、结果区域、popup 和 selector。src/AtomUI.Desktop.Controls/Tooltip/OverflowTip.cs:共享溢出 tooltip attached behavior,供单选结果和多选 tag 复用。
3. 核心类职责
TreeSelect 是树形选择协调器。它维护 Items、ItemsSource、SelectedItem、SelectedItems、EffectiveSelectedItems、过滤值、勾选策略和 popup 树内容同步。
TreeSelectAddOnDecoratedBox 是 TreeSelect 输入布局扩展。它复用 InputControlFrame 的 variant、effective status、Addon、CompactSpace 和尺寸能力,并额外承载 IsMultiple、IsSelectionEmpty 和 IsDropDownOpen;不重新定义输入表面状态优先级。
TreeSelectTreeView 是候选树边界。它继承 AtomUI TreeView,复用 TreeView 的视觉和交互能力,并把 IsMaxSelectReached 转发到 TreeViewSelectTreeViewItem。
TreeViewSelectTreeViewItem 是候选节点容器。它继承 TreeViewItem,并把最大选择数状态继续传给子节点容器,保证层级节点状态一致。
OverflowTip 不参与 TreeSelect 选择同步。TreeSelect 模板把单选 SelectFilterTextBox.Text、多选 SelectTagAwareTextBox 创建的 tag 文本以及 OverflowTipDelay / OverflowTipPlacement 接入该 behavior;单选显示节点以外层 TreeSelectAddOnDecoratedBox 作为 PlacementTarget,避免 tooltip 左边按内部文本 padding 对齐;它只在展示文本溢出时写入自己托管的 ToolTip.Tip,用户显式 tooltip 保持最高优先级。
4. 状态与数据流
初始化流:
TreeSelect()
→ RegisterTokenResourceScope(TreeSelectToken)
→ Items.CollectionChanged += HandleItemsChanged
OnInitialized()
→ if Filter == null, use contains filter
→ ConfigureMaxSelectReached()
数据源流:
ItemsSource changed
→ remember current selected node path
→ Items.SetItemsSource(new source)
→ remap selected node by ItemKey or Value path
→ clear selection when path cannot be resolved
选择同步流:
TreeSelectTreeView.SelectionChanged / CheckedItemsChanged
→ SelectedItem or SelectedItems
→ NotifyFormValueChanged
→ SelectedCount / placeholder / max count / effective items
→ sync back to TreeView when public selection changes
SelectedItems ObservableCollection.CollectionChanged
→ NotifyFormValueChanged
→ refresh placeholder / SelectedCount / EffectiveSelectedItems
→ sync TreeView.SelectedItems or TreeView.CheckedItems
→ refresh max count state
过滤流:
SelectFilterTextBox.TextChanged
→ trim text
→ FilterValue = text or null
→ TreeView receives FilterValue / Filter / FilterStrategy
→ TreeView applies highlight, bold, expand path and hide unmatched strategy
Form 流:
Form.SetValue(object?)
→ Single: SelectedItem
→ Multiple / checkable: SelectedItems
Form.GetValue()
→ Single: SelectedItem
→ Multiple / checkable: SelectedItems
Form.ClearValue()
→ clear current selection
DataValidationErrors
→ InputControlFrame.EffectiveStatus
ValidateStatus
→ FormStatus → InputControlFrame + feedback
FeedbackControl
→ FormFeedback → SelectHandle
5. 生命周期与模板接入
AbstractSelect.OnApplyTemplate() 负责稳定 popup part 接入、弹层最大高度计算、popup opened / closed 订阅和输入壳体基础 part 获取。
TreeSelect.OnApplyTemplate() 在基类接入前调用 ClearPopupContent(),确保旧 TreeSelectTreeView、popup frame 和事件订阅被释放。基类接入后,TreeSelect 获取 PART_SingleFilterInput,设置 Popup.OverlayInputPassThroughElement,并重新配置选择空状态、伪类、过滤输入、placeholder、单选结果、右侧 handle sibling 状态 binding 和已打开 popup 内容。
候选弹层内容是懒创建:
OpenDropDown()
→ EnsurePopupContent()
→ create PopupFrame if null
→ create TreeSelectTreeView if null
→ bind tree view to TreeSelect properties
→ subscribe SelectionChanged / CheckedItemsChanged
→ Popup.Child = PopupFrame
ClearPopupContent() 必须解除 _treeView.SelectionChanged 和 _treeView.CheckedItemsChanged,清空 ItemsSource,移除 TemplatedParent,断开 PopupFrame.Child 和 Popup.Child,并清空 _treeView / _popupFrame。
6. 交互与事件处理
指针路径:
- popup 内部点击不关闭弹层。
- 多选 tag 的关闭按钮点击不触发外层打开/关闭切换。
- 控件按下时设置标准
:pressed,释放时根据状态打开或关闭弹层。 - 单选模式中点击树节点后关闭弹层。
搜索输入路径:
- 单选且 popup 打开并启用过滤时,
PART_SingleFilterInput可编辑。 - 单选结果展示态下,过滤输入只读并显示当前选中节点文本。
- 多选模式下,单选过滤输入不参与编辑,结果由
SelectedItemsBox承载。
tag 关闭路径:
SelectTag.Closed
→ locate ITreeItemNode from tag.Item
→ remove node and descendants from copied SelectedItems
→ assign SelectedItems copy
7. 内部算法与关键流程
7.1 节点路径重映射
ItemsSource 替换前,TreeSelect 将当前选择转换为 TreeNodePath。路径每一段优先使用节点 ItemKey,没有 ItemKey 时使用 Value。新数据源写入 Items 后,控件沿路径逐层查找新节点实例,并把选择映射到新实例。
7.2 选择同步抑制
TreeSelect 有两个内部同步状态:
_needSkipSyncSelection:TreeView 事件正在写入 public selection 时,避免 public selection 再回写 TreeView。_needSkipCollectionChangedEvent:多选 selection 回写 TreeView.SelectedItems 时,避免集合事件再次写回 public selection。
这两个状态只用于确定性的同步闭环,必须通过 try/finally 成对进入和退出。维护时不能把它们扩展成通用“忽略下一次事件”的补丁开关。
7.3 勾选展示集合
BuildEffectiveSelectedItems() 根据 ShowCheckedStrategy 从真实 SelectedItems 派生展示集合:
All直接展示SelectedItems。ShowParent检测父节点子节点是否全部选中,并隐藏已展示父节点下的子节点。ShowChild只展示叶子节点。
派生结果写入 internal EffectiveSelectedItems,供模板中的 SelectedItemsBox 展示。
7.4 最大选择数
ConfigureMaxSelectReached() 在 IsMultiple=true 且 SelectedItems.Count >= MaxCount 时设置 IsMaxSelectReached=true。该状态通过 TreeSelectTreeView 传给候选节点容器,用于阻止继续选择未选节点,同时保留取消已选节点的路径。
7.5 弹层尺寸
弹层最大高度由 AbstractSelect 根据 ItemHeight、DisplayPageSize 和 PopupContentPadding 计算。TreeSelect 自身通过 TreeSelectToken.MinPopupWidth 给候选弹层提供下限宽度,并在 IsPopupMatchSelectWidth=true 时继承输入控件当前宽度作为有效最小宽度。
7.6 右侧内容和 handle binding
TreeSelect 根模板中的稳定 template part 关系优先由 AXAML 表达:
SelectMaxCountIndicator通过 compiled ancestor binding 接收MaxCount、SelectedCount和IsShowMaxCountIndicator。PART_ContentRightAddOnPresenter通过 compiled ancestor binding 接收ContentRightAddOn、ContentRightAddOnTemplate,并用ObjectConverters.IsNotNull控制显示。SelectHandle通过 compiled ancestor binding 接收 Form feedback、loading 图标、展开图标、过滤状态、启用状态、motion、loading、clear、选择为空和下拉打开状态。
SetupSelectHandleInputStateBindings() 只保留 frame layout part 到 SelectHandle 的 hover / pressed sibling 状态转发。这个关系的 source 是同一模板中的 sibling part,不是 templated parent,无法用 TemplateBinding 或 ancestor binding 在不改变模板契约的前提下表达;它只转发交互状态,不改变 InputControlFrame 对 variant、effective status、error、warning 或 disabled 的唯一 ownership。该 binding 在每次重新应用模板前释放旧 CompositeDisposable,生命周期与模板 part 获取路径一致。
8. 资源、性能与 AOT 边界
TreeSelect 不依赖运行时反射发现模板结构。模板协作通过固定 template part、显式类型、AXAML binding、C# indexer binding 和事件完成。
资源和生命周期边界:
_selectHandleInputStateBindings每次模板接入前释放旧绑定,仅持有 frame layout part → SelectHandle 的 hover / pressed sibling 状态转发,不承载输入表面状态归一。_treeView的事件订阅和ItemsSource必须在ClearPopupContent()中释放。- 懒创建的
PopupFrame和TreeSelectTreeView必须设置TemplatedParent,并在清理时置空。 Items.CollectionChanged是控件实例持有自身集合的订阅,生命周期与控件实例一致。_selectedItemsCollectionChangedSource只订阅当前SelectedItems中实现INotifyCollectionChanged的集合;SelectedItems替换、控件 detach 时必须释放旧订阅。- TreeView 异步加载由 TreeView 家族处理,TreeSelect 不直接持有异步任务状态。
AOT 边界:
TreeSelectToken通过 token generator 显式注册,生成TreeSelectTokenKind和TreeSelectTokenResourceExtension。ItemTemplate是 XAML 模板入口,不依赖运行时成员扫描。- API 与 Token 契约由控件文档、源码 public surface、Token 类型或生成数据维护。
9. 维护不变量
内部重构必须保持以下不变量:
- public/protected API、Avalonia property、template part、Token 和 selector 契约不变。
- 构造函数之后必须先放 TreeSelect 自身 public/protected API,再放 Form 接口区,private 实现方法必须位于接口区之后。
- 可用 AXAML 表达的模板绑定不能回退为
BindUtils.RelayBind。 - C# relay binding 必须有与获取路径匹配的释放路径。
SelectedItem/SelectedItems与 TreeView selection / checked items 的同步不能形成递归事件。SelectedItems集合引用替换和原地变更必须刷新同一组 value-state,避免 tag、SelectedCount、Form 值和 popup TreeView 状态不同步。ShowCheckedStrategy只能派生展示集合,不能改写真实选择集合。ItemsSource替换时的选择保留必须继续使用节点路径 identity。- popup 内容清理必须断开事件、ItemsSource、TemplatedParent 和 popup child 引用。
10. 测试与验证
对应验证:
tests/AtomUI.Desktop.Controls.Tests/TreeSelect/TreeSelectBehaviorTests.cs:模板右侧 AXAML binding 和剩余 sibling relay binding。tests/AtomUI.Desktop.Controls.Tests/TreeSelect/TreeSelectSelectionBindingTests.cs:SelectedItem/SelectedItems默认双向绑定、data validation metadata、SelectedItems原地变更和 checkable TreeView 同步。tests/AtomUI.Desktop.Controls.Tests/SizeType/CustomizableSizeTypeContractTests.cs:TreeSelect 的ICustomizableSizeTypeAware契约。tests/AtomUIGallery.Tests/ShowCases/TreeSelectShowCasePageTests.cs:Gallery TreeSelect 页面结构、示例和源码片段约束。
维护 TreeSelect 后至少运行:
dotnet test tests/AtomUI.Desktop.Controls.Tests/AtomUI.Desktop.Controls.Tests.csproj --framework net10.0 --no-restore
git diff --check
涉及 Gallery 示例或文案时,追加运行:
dotnet test tests/AtomUIGallery.Tests/AtomUIGallery.Tests.csproj --framework net10.0 --no-restore --filter FullyQualifiedName~TreeSelectShowCasePageTests