Files

14 KiB
Raw Permalink Blame History

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