Files
AtomUI/docs/controls/desktop/data-entry/tree-select/implementation.md
T

13 KiB
Raw Blame History

TreeSelect 桌面版实现原理

本文档描述 TreeSelect 桌面版的输入壳体、树数据集合、选择同步、过滤、候选弹层、Form 和 Token 资源边界。公共设计与 API 契约见 TreeSelect 桌面版架构设计,Token 语义见 TreeSelect Token 设计,变化记录见 TreeSelect Changelog。

1. 实现定位

TreeSelect 的实现以 AbstractSelect 为输入与弹层基类,TreeSelect 本体负责树数据、树选择、过滤输入和结果展示协调。树节点容器、展开、勾选和异步加载由 TreeView 家族承担。本文档聚焦 TreeSelect 自身的状态编排、生命周期和维护边界,不重新说明 TreeView 的通用容器回放、AddOnDecoratedBox 的通用输入外观或 PopupHost 的全局资源规则。

2. 源码文件结构

主要源码:

  • src/AtomUI.Desktop.Controls/TreeSelect/TreeSelect.cs:public TreeSelect API、生命周期、弹层树懒创建、选择同步、过滤输入、Form 映射和私有算法。
  • src/AtomUI.Desktop.Controls/TreeSelect/TreeSelectAddOnDecoratedBox.cs:TreeSelect 输入壳体内部状态,扩展 AddOnDecoratedBox 的多选和选择空状态。
  • 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 输入表面边界。它继承 AddOnDecoratedBox 的 variant、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
  → native error visual + AddOn effective error state
ValidateStatus
  → Warning/Success/Validating extension state
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() 只保留 AddOnDecoratedBox 到 SelectHandle 的 hover / pressed 状态转发。这个关系的 source 是同一模板中的 sibling part,不是 templated parent,无法用 TemplateBinding 或 ancestor binding 在不改变模板契约的前提下表达。该 binding 在每次重新应用模板前释放旧 CompositeDisposable,生命周期与模板 part 获取路径一致。

8. 资源、性能与 AOT 边界

TreeSelect 不依赖运行时反射发现模板结构。模板协作通过固定 template part、显式类型、AXAML binding、C# indexer binding 和事件完成。

资源和生命周期边界:

  • _selectHandleInputStateBindings 每次模板接入前释放旧绑定,仅持有 AddOnDecoratedBox → SelectHandle 的 hover / pressed sibling part 状态转发。
  • _treeView 的事件订阅和 ItemsSource 必须在 ClearPopupContent() 中释放。
  • 懒创建的 PopupFrame 和 TreeSelectTreeView 必须设置 TemplatedParent,并在清理时置空。
  • Items.CollectionChanged 是控件实例持有自身集合的订阅,生命周期与控件实例一致。
  • _selectedItemsCollectionChangedSource 只订阅当前 SelectedItems 中实现 INotifyCollectionChanged 的集合;SelectedItems 替换、控件 detach 时必须释放旧订阅。
  • TreeView 异步加载由 TreeView 家族处理,TreeSelect 不直接持有异步任务状态。

AOT 边界:

  • TreeSelectToken 通过 token generator 显式注册,生成 TreeSelectTokenKind 和 TreeSelectTokenResourceExtension。
  • ItemTemplate 是 XAML 模板入口,不依赖运行时成员扫描。
  • Gallery API / Token 表使用显式 ViewModel 数据。

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 页面结构、示例和 API / Token 表约束。

维护 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