37 KiB
DataGrid 桌面版实现原理
本文档描述 DataGrid 桌面版的内部 ownership、源码职责、状态流、生命周期、资源边界和维护规则。公共设计与 API 契约见 DataGrid 桌面版架构设计,不可变查询、范围数据源、异步协调和虚拟化算法见 DataGrid Query 与 Range Source 设计,列宽算法和 presenter 协作见 DataGrid 列宽分配设计,Control Own Token 见 DataGrid Token 设计,变化记录见 DataGrid Changelog。
Popup 接入边界:DataGrid 负责 column-filter Query intent 与候选内容,filter Flyout 只作为 relay 适配层,filter Popup 负责
实际显示。模板重建或宿主切换时先释放旧 relay,再绑定新的 Popup;普通外点、Escape、失焦和业务关闭在 pinned 状态下被
拦截,detach、窗口销毁、跨 TopLevel 和无效锚点走生命周期关闭并释放 Popup host。完整状态机见
Popup 钉住打开设计。
1. 实现定位
DataGrid 的实现同时维护四条互不争夺 ownership 的链路:
- Data:不可变 Query、Source schema、range request/result、snapshot 和 selection。
- Presentation:range cache、display slot、已提交 viewport、row/group container 与 scroll extent。
- Columns:列定义、列宽、冻结列、水平虚拟化和 header/cell 视觉投影。
- Theme:ControlTheme、Template Part、伪类、Token、Popup 和平台宿主。
DataGrid 是 public contract 与 UI transition owner;Source 执行业务查询;presenter 只布局已经提交的数据;template part 只投影 状态。任何功能都不能通过在这些层之间建立第二份可变状态来完成同步。
2. 源码文件结构
稳定 ownership 按职责组织:
src/AtomUI.Desktop.Controls.DataGrid/
├── DataGrid.cs public contract + lifecycle
├── DataGrid.Query.cs Query commit and visual projection
├── DataGrid.RangeLoading.cs viewport-to-coordinator integration
├── DataGrid.Virtualization.cs desired/committed viewport + container bridge
├── Data/
│ ├── Query/ immutable values, scalar and validation
│ ├── Source/ schema, request, result and capability contracts
│ │ └── Local/ typed local source and projection
│ └── Virtualization/ viewport scope, request scheduler, snapshot, cache, index and heights
├── Column/ column contract, header, sorting/filtering interaction
├── Column/Filters/ filter indicator, flyout and candidate presentation
├── Row/ row, group header, details and row presenters
├── Cell/ cell state and horizontal virtualization
├── EventArgs/ public event payloads
├── Themes/ static templates, selectors and resource binding
├── Localization/ generated-catalog-backed localized strings
└── Utils/ narrow shared algorithms and converters
维护规则:
- 主文件保留 public/protected API、Avalonia 属性注册、事件和主要生命周期入口。
- partial 与 helper 按稳定 owner 拆分,不按 public/private 或单个触发点拆分。
- Theme 文件负责静态视觉结构、Template Part、selector 和资源绑定;不在 C# 中动态复制同一结构。
- Token 只保存组件视觉变量,不保存 Query、loading、selection、expanded 或 popup runtime state。
- Gallery 只展示用法和验证行为,不成为运行时状态 owner。
- GeneratedFiles 由对应 generator 维护,不手工编辑。
3. 核心类职责
| 类型 | Ownership | 主要输入与输出 |
|---|---|---|
DataGrid |
public API、模板生命周期、presentation transition | 接收 Query/Source/input,发布 applied state 与视觉更新。 |
DataGridQueryController |
Query validation、revision 和交互策略 | 把外部设置、sort/filter/group intent 归一为唯一 Query。 |
IDataGridSource |
schema、range fetch、snapshot 与 invalidation | 消费 DataGridFetchRequest,返回不可变精确切片。 |
DataGridRangeCoordinator |
generation、viewport scope、请求优先级、block lease、pending cache 和错误 | 保留最新目标仍需要的工作,取消过时工作,确保 visible range 覆盖并形成可提交 snapshot。 |
DataGridPresentationSnapshot |
一次完整 applied presentation | 固定 Source/query/generation/snapshot、page、expansion、totals 与 viewport。 |
DataGridPresentationIndex |
slot/entry/offset 映射 | 管理有限 block、sparse height delta 与 key/index metadata。 |
DataGridDisplayData |
已实现纵向容器和回收池 | 在 committed range 上复用 row/group header,不读取 Source。 |
DataGridRowsPresenter |
同步纵向布局与 child index | 只测量/排列 committed containers。 |
DataGridCellsPresenter |
可见列和冻结列布局 | 维持水平 virtualization,不拥有纵向 range 请求。 |
DataGridColumn |
FieldId、列级能力、SortState | 从 Query/schema 推导 header 与 cell 状态。 |
DataGridLocalSource<T> |
本地 typed projection | 在稳定快照上 filter/sort/group,并只物化请求 range。 |
协作边界:
- Source 不捕获 DataGrid,也不返回可继续修改的 entry collection。
- Coordinator 不解释业务协议;LocalSource 和业务 Source 不生成视觉对象。
- RowsPresenter、DisplayData、Row 和 Cell 不发起 FetchAsync。
- Header、FilterIndicator 和 Flyout 只产生 Query intent,不直接改 range cache。
- 可选 editable、movable、key-lookup 和 bulk-selection 能力与只读 Source 分离。
- DataGrid 是列宽 solver owner;header/rows/cells presenter 只提供有限 viewport 或内容测量输入。
4. 状态与数据流
4.1 Query 到 presentation
Public Query / column gesture / filter confirmation / group intent
-> DataGridQueryController validates structure and Source.Schema
-> commit pending edit
-> structural no-op check
-> QueryRevision++ and visual projection
-> QueryChanged
-> DataGridRangeCoordinator
-> IDataGridSource.FetchAsync
-> result contract validation
-> atomic DataGridPresentationSnapshot commit
-> rows / groups / pagination / scrollbar / selection projection
Query 是 sort/filter/group 的唯一 owner;AppliedQuery 只属于已提交 snapshot。QueryChanged handler 若同步设置另一个 Query,新的 revision 使外层 transition 失效。没有 suppress flag、ignore flag 或 Dispatcher 延时。
Source replacement、Reload、Invalidated、snapshot expiry、PageRequest 与 GroupExpansion 变化递增 DataGeneration。每个 request 同时携带 QueryRevision 与 DataGeneration,任何迟到或来自旧 Source/snapshot 的结果在 commit 前丢弃。
4.2 Desired 与 committed viewport
scroll / thumb / keyboard / bring-into-view intent
-> coalesced DesiredViewport
-> replace active ViewportRequestScope
-> pure ViewportPlanner
-> retain shared visible block leases and cancel orphaned work
-> foreground scheduler fills missing blocks (synchronous hit or asynchronous miss)
-> atomic CommittedViewport swap
-> one layout pass over committed blocks
DesiredViewport 是最新用户意图;CommittedViewport 是当前可以同步布局的完整范围。缓存 miss 时继续显示旧 committed rows, 不创建 placeholder 或 null row。同一 generation 只有一个 active viewport scope;新 scope 先取得仍需 block 的 lease,再释放旧 scope,避免相邻目标取消并重取共享 block。失去所有有效 lease 的工作从可发现 inflight 集合移除并取消,后续请求不会附着到 已经取消的 task。prefetch 从属于产生它的 committed scope,只填 cache,不改变 loading visual、不触发布局、不创建 control, 也不能阻塞新的 visible target。
4.3 Load 与错误状态
ItemsSource == null -> Idle
no applied snapshot + active request -> Loading
applied snapshot + active request -> Refreshing
visible target atomically committed -> Ready
non-cancellation failure -> Error
internal EffectiveIsOperating 只合并用户显式 IsOperating 与 Loading:
EffectiveIsOperating = IsOperating || LoadState == Loading
Loading 表示没有可展示的已提交 snapshot,实际挂起时驱动现有 Spin。Refreshing 保留旧 rows 的几何与完整不透明度,
不自动启动 Spin;读取类 selection 可以继续投影,edit/delete/move 仍被禁止。调用方显式设置 IsOperating=true 时,任意
LoadState 都继续显示 Spin。
只有 ValueTask 实际挂起时才发布 Loading/Refreshing。同步 Source 与 cache hit 在当前调用中完成 result validation、snapshot commit 和 Ready 投影,不增加一次 Dispatcher 调度,也不让嵌套 DataGrid 在一帧内停留于虚假的 Loading。
用户 Query/page/group/scroll 失败时恢复最后成功 snapshot 的 Query、page、expansion、viewport、totals 和视觉;只有实际 Query 回退发出一次 LoadRollback QueryChanged。初次失败保持空 presentation。Invalidated 刷新失败保留 stale 只读 snapshot 并等待 Reload。Cancellation 和 superseded request 不进入 Error。
4.4 选择与 current
DataGridSelectionState + CurrentRowKey
-> committed DataGridSourceEntry(RowKey, DataIndex)
-> realized DataGridRow/DataGridCell pseudo-classes
Selection 使用 ExplicitKeys、AllMatchingQuery、IndexIntervals 和 ExcludedKeys 表达声明式状态,内存不随 total row count 增长。
Row/Cell 只投影当前 entry 的判定结果,container recycle 不改变 selection。CurrentRowKey 是 identity;DataIndex hint 只用于定位。
未加载 key 的解析依赖可选 IDataGridKeyLookupSource,不能伪造 SelectedItem 或 index。
4.5 分页
PageSize / current page intent
-> checked DataGridPageRequest(long data start, int count)
-> DataGridFetchRequest
-> applied TotalDataCount and page state
-> PART_TopPagination / PART_BottomPagination
PageSize 为 0 时使用连续模式。非零时先按业务行切 PageRequest,再插入 group header、应用 expansion 和 display Range。上下两个 Pagination 只投影同一 applied state;模板 reapply 先回放 state,再订阅 input。翻页成功后 vertical offset 归零,失败时整体 回退。
4.6 列宽
DataGridColumnHeadersPresenter / DataGridGroupColumnHeadersPresenter
-> header desired widths + finite viewport when rows are absent
DataGridRowsPresenter / DataGridCellsPresenter
-> CellsWidth + realized cell desired widths when rows are present
DataGrid
-> finish Auto measurement
-> AdjustColumnWidths resolves star widths
DataGridColumn display widths
-> headers / rows / cells / filler / scrollbars
普通与分组列头共享有限宽度入口;有行时使用 CellsWidth。Presenter 不独立修改 star 列,不把 filler 当作求解结果。完整矩阵见 DataGrid 列宽分配设计。
4.7 过滤与 pinned popup
Column.Filters / presentation metadata
-> filter Flyout Query intent
-> immutable DataGridQuery.Filters
-> Source request + Header/Indicator/Flyout projection
Filters 只表示候选内容;Query.Filters 是 applied 条件 owner。Flyout 打开时从 Query 初始化 checked state,确认时构建一个完整新 Query。Schema 声明 operator、arity 和 scalar kinds;LocalSource descriptor 提供 typed evaluator,远端 Source 显式翻译。
Pinned filter 由 DataGrid 独占 ownership:
DataGrid.IsPopupPinnedOpen
-> first eligible column in DisplayIndex order
-> DataGridColumnHeader
-> DataGridFilterIndicator
-> current filter Flyout
-> Popup
replacement 先 lifecycle-close 旧 Popup、释放 relay 和 callback,再接入新目标。Unpin 只解除关闭拦截;DataGrid detach、template reapply、禁用、隐藏、TopLevel 变化或锚点失效强制关闭旧 Popup。
4.8 行拖动
PointerPressed
-> RowReorderSession(Pressed, pointer, Source, snapshot, RowKey)
-> drag threshold
-> RowReordering
-> revalidate owner/query/snapshot/capability
-> RowsPresenter ghost row
-> key-relative IDataGridMovableSource request
-> cleanup
-> RowReordered only after successful mutation
每个 DataGrid 只有一个会话。handle 不修改数据,presenter 不决定移动语义,Source 不持有视觉对象。Source、Query、snapshot、 row、pointer 或 capability 变化均取消会话。完成和取消共用幂等清理入口,释放 capture、ghost、offset、transition 和引用。
5. 组合结构模型
5.1 控件角色图
DataGrid (DataGridTheme.axaml)
-> PixelAlignedBorder#Frame (template-stable)
-> Border#FrameContentClip (template-stable)
-> Spin (internal-observable)
-> DockPanel
-> Pagination#PART_TopPagination (template-stable)
-> PixelAlignedBorder#TitleFrame
-> ContentPresenter#Title
-> Pagination#PART_BottomPagination (template-stable)
-> ContentPresenter#Footer
-> Grid
-> DataGridTopLeftColumnHeader#PART_TopLeftCorner (template-stable)
-> Border#ColumnHeadersPresenterFrame
-> Panel
-> DataGridColumnHeadersPresenter#PART_ColumnHeadersPresenter (template-stable)
-> DataGridGroupColumnHeadersPresenter#PART_GroupColumnHeadersPresenter (template-stable)
-> PixelAlignedBorder#ColumnHeadersAndRowsSeparator
-> DataGridRowsPresenter#PART_RowPresenter (template-stable)
-> DataGridRow (internal-observable, virtualized)
-> DataGridCellsPresenter (internal-observable, horizontally virtualized)
-> DataGridRowGroupHeader (internal-observable, virtualized)
-> ContentPresenter#EmptyIndicator
-> ScrollBar#PART_VerticalScrollbar (template-stable)
-> ScrollBar#PART_HorizontalScrollbar (template-stable)
-> Border#DisabledVisualElement
-> DataGridColumnDraggingOverIndicator#PART_DraggingOverIndicator (template-stable)
模板中的 Spin 节点复用 DataGrid 的 operating state;Query/Range Source 只改变该状态的有效输入,不增加平行 overlay。RowsPresenter 下面的 row、group header 和 cells 由 container lifecycle 动态接入,但必须继续遵守同一个 template/scroll ownership。
5.2 协作节点
| 节点 | 类型 | 来源 | 生命周期 owner | 影响的 public API | 稳定性 | 使用边界 |
|---|---|---|---|---|---|---|
| DataGrid | public control | DataGrid.cs |
应用/VisualTree | Source、Query、Selection、layout/theme API | public | 唯一 UI state owner。 |
| Frame / FrameContentClip | template node | DataGridTheme.axaml |
DataGrid template | border、corner、background | template-stable | 维护外框和内容裁剪,不能下沉到 row。 |
| Spin | public child control | DataGridTheme.axaml |
DataGrid template | IsOperating、LoadState | internal-observable | 只投影 effective operating state。 |
| Top/Bottom Pagination | public child control | DataGridTheme.axaml |
DataGrid template | PageSize、visibility、align | template-stable | 只投影 applied page state。 |
| Column headers presenters | internal presenters | DataGridTheme.axaml |
DataGrid template | headers、column width、Query intent | template-stable | 提供测量与交互,不拥有 Query。 |
| RowsPresenter | internal presenter | DataGridTheme.axaml |
DataGrid template | rows、scrolling、selection | template-stable | 只布局 committed entries,不调用 Source。 |
| Row / GroupHeader | item containers | C# container generation | DataGridDisplayData | item/group/details/selection | internal-observable | recycle 时完整清理 entry state。 |
| CellsPresenter | internal presenter | Row theme/C# | row container | columns、frozen、horizontal scroll | internal-observable | 继续拥有横向 virtualization。 |
| EmptyIndicator | template node | DataGridTheme.axaml |
DataGrid template | EmptyIndicator | internal-observable | 只由 committed empty state 控制。 |
| ScrollBars | template nodes | DataGridTheme.axaml |
DataGrid template | scrollbar visibility | template-stable | 与 PresentationIndex 的 extent/offset 同步。 |
| DraggingOverIndicator | internal control | DataGridTheme.axaml |
row reorder session | CanUserReorderRows | template-stable | 只绘制反馈,不提交数据。 |
| Filter Flyout / Popup | internal overlay chain | filter themes/C# | current header/indicator/flyout | column filter options、pinned state | internal-observable | relay Query intent,replacement 对称释放。 |
6. 生命周期与模板接入
6.1 控件生命周期
- 构造阶段注册属性与静态状态,不依赖 Template Part。
- OnApplyTemplate 开始先解绑旧 part,再获取新 part、回放 applied state 并建立事件连接。
- Attach 时订阅当前 Source.Invalidated;Source replacement 与 detach 时对称退订。
- Detach 先失效 revision/generation identity,再取消 request、dispose CTS、释放 pending/applied cache、part 与 relay。
- 迟到 continuation 必须核对 Source、revision、generation 和 snapshot;identity 失效后直接结束。
- DataGrid 不 dispose 外部 Source;Source 自己拥有数据库连接、集合订阅和自身 IDisposable/IAsyncDisposable 生命周期。
6.2 Source replacement
Source replacement 遵循预验证事务:候选 schema、当前 Query、列与 page 首先在旧状态外验证;随后提交 edit;只有这些步骤成功 才一次性切换 Source identity 和 generation。切换后取消/退订旧 owner、清空旧 cache/totals/key resolution,订阅新 Source 并 加载首个范围。切换点之后旧 Source 永不重新成为有效 owner。
6.3 Request 与 cache 生命周期
| 资源 | Owner | 释放或失效 |
|---|---|---|
| generation CTS | RangeCoordinator | Query/generation change、detach、完成后 Dispose |
| viewport request scope | RangeCoordinator | 不同 DesiredViewport、generation replacement 或 detach 时 supersede;先转移共享 lease,再释放其余 lease |
| block request lease | active viewport scope | scope supersede、visible commit 后 prefetch 失效、generation replacement 或 detach |
| block work item CTS | block request scheduler | 最后一个有效 lease 释放或 generation/detach 失效时取消;work item 进入终态后 Dispose |
| Source request CancellationToken | 单 block work item | 由 generation 与 work item 生命周期共同约束;不受已失效 caller continuation 复活 |
| pending blocks | pending snapshot | commit、failure、query/source change、detach |
| applied blocks | applied snapshot | snapshot/source change、detach |
| realized/edit/drag block pin | 对应 visual/session owner | container/session detach 后 |
| pooled entry/index arrays | LocalSource 或 block | projection/block eviction 或 Source dispose |
| sparse height entries | PresentationIndex block/user state | block eviction 或显式 state 清理 |
6.4 Template Part 接入
PART_RowPresenter只承载 committed row/group containers;空数据隐藏,不成为列宽求解前置。- 显式有限高度的首个 snapshot 可以先只提交数据,由首次布局按真实 viewport 实现 rows;自动高度且尚无 Bounds 的嵌套 DataGrid 在模板就绪后实现一个已提交 bootstrap entry,以建立非零 DesiredSize,期间不从 Measure 请求 Source。
PART_TopPagination与PART_BottomPagination先接收 applied state,再订阅翻页 intent。PART_ColumnHeadersPresenter与PART_GroupColumnHeadersPresenter接入同一个 DataGrid-owned column-width solver。- 现有 Spin 绑定 EffectiveIsOperating;首次异步 loading 显示 Spin,refreshing 保持已提交内容完全可见,两者都不改变 Frame/Header/scroll geometry。
- SortIndicator、FilterIndicator、row/cell/group header 继续使用既有 theme、part 与 pseudo-class。
- FrameContentClip、FrameCornerRadius 和 FrameBorderThickness 的 ownership 不因异步数据架构改变。
7. 交互与事件处理
- Header pointer、keyboard、tooltip 与
SetSort都调用同一DataGridSortPolicy;resize 边界命中优先于排序手势。 - Filter Flyout 只收集候选值并提交 Query intent;Query validation、edit commit 和 request scheduling 仍由 DataGrid 管理。
- 滚轮、触控惯性、scrollbar line/page/thumb、Home/End/PageUp/PageDown 和 ScrollIntoView 统一产生 DesiredViewport。
- Scroll handler 只合并 intent;到内部边界前由 DataGrid 消费,边界后允许外层 ScrollViewer 继续滚动。
- QueryChanged 在内存提交与视觉投影后、Source request 前触发;回调重入后重新核对 revision。
- SelectionChanged 描述声明式 OldSelection/NewSelection,不为事件参数加载 cache 外 item。
- RowReordering 在超过拖动阈值且创建 ghost 前最多触发一次;回调后重新验证 Source、Query、snapshot、row 和 capability。
- RowReordered 只在 Source mutation 成功且 capture、ghost、offset、transition 和会话引用清理后触发。
- Disabled、detach、template reapply、Source/query replacement、capture lost 或 row recycle 终止当前 mutation/drag session。
- Loading/Refreshing/stale 阻止 edit/delete/move 提交,但不能截断滚动 intent 的合并或破坏现有 focus/keyboard 路径。
8. 内部算法与关键流程
8.1 Source result validation
UI commit 前验证:
- Source identity、QueryRevision、DataGeneration、request Range 与 ExpectedSnapshot 匹配。
- StartIndex、Entries.Length 与 totals 满足精确切片公式。
- Data/Group entry 的 payload、RowKey/GroupKey、WindowDataIndex 与 DataIndex 合法。
- 当前 cache 范围内 row/group key 分域唯一且 data indices 严格递增。
- 相同 snapshot 下 TotalDataCount、WindowDataCount 和 TotalEntryCount 的条件恒定。
任何合同错误进入 DataGridSourceContractException,不部分提交。连续模式不能用 int 精确表示活动 data/display count 时抛出
DataGridPresentationLimitExceededException,不能截断、饱和或取模。
8.2 三个索引域
| Domain | 类型 | 使用位置 |
|---|---|---|
| Source data | long | TotalDataCount、PageRequest.DataStartIndex、entry.DataIndex |
| Window data | int | WindowDataCount、entry.WindowDataIndex、DataGridRow.Index、IChildIndexProvider |
| Display slot | int | TotalEntryCount、Range.StartIndex、DataGridRow.Slot、vertical virtualization |
不同 domain 只通过 committed entry 显式映射。GroupHeader 不占 WindowDataIndex。分页可以访问 long global index,但单个 UI window 的 child/slot count 必须保持准确 int。
8.3 Range planning、cache 与 commit
Block size 由 Schema.PreferredRangeSize 在 [32, MaximumRangeSize] 内确定。generation 尚未取得 result identity 时,bootstrap
是 generation 级前置工作,不随每个中间 viewport 重启;identity 建立后只为最新 target 补齐 visible block。最大 Source 并发
为 2。
Coordinator 在锁内完成 scope 交换和 lease 转移,在锁外执行取消与 Source 调用,避免业务 Source continuation 或 cancellation callback 进入内部 gate。新 scope 先取得仍需 visible block 的 lease,再释放旧 scope;没有 lease 的排队 work item 立即从调度队列 和可发现 inflight 集合移除,没有 lease 的执行中 work item 立即取消。相同 generation/block 的有效 work item 继续去重;已经标记 取消的 work item 不得被新 scope 复用,其迟到结果也不能写入 active pending cache、错误状态或 presentation。
调度器维护 visible 与 prefetch 两个有界优先级。空闲并发槽始终先取 visible;prefetch 只在没有 visible 缺口时进入 Source。 新 visible intent 会先撤销旧 scope 的 prefetch lease,再调度自身缺口。已经进入 Source 的旧工作依赖 Source 协作取消释放槽位, 但无论 Source 是否遵守取消,迟到结果都不能通过 generation、scope 与 lease identity 校验。
同步 Source 或 cache hit 仍沿当前调用栈完成,不被优先级队列强制异步化。caller cancellation 只结束该 caller 的等待;只要同一 work item 仍被 active scope 使用,就不能由单个 waiter 取消共享 Source 工作。scope supersede 和 generation 失效才改变 work item 的有效 lease。旧 scope completion 不得覆盖新 scope 的 Loading/Refreshing;prefetch failure 只保留诊断信息,不改变可见状态。
Cache 是有限 LRU,容量不依赖 TotalDataCount。prefetch block 不生成 control。visible blocks 完整且全部通过验证后一次性 pin 新 block、交换 snapshot/viewport、更新 totals、回收旧容器、实现新容器、回放 selection/current/edit/details、更新 extent/offset/scrollbar 并进入 Ready。旧容器 detach 后才解除旧 block pin。
若 totals 变化导致 page/scroll target 越界,丢弃 pending presentation,递增 generation 并只请求最终合法 target,不能显示 瞬时空页。
8.4 Height、extent 与 anchor
固定行高使用 O(1) offset/slot 公式。自动行高、GroupHeader 和 RowDetails 使用:
estimatedOffset(slot) = slot * defaultEstimate + sparsePrefixDelta(slot)
SparseHeightDeltaIndex 只保存 pinned/current-LRU block 的实测差值;passive block eviction 时把样本吸收到常量大小、按 entry kind/group level 分类的 estimator 并删除明细。prefix sum 和 offset-to-slot 是 O(log M),M 受 cache/pin 与用户显式状态上限 约束。禁止从 slot 0 扫描到 first slot,也禁止按 total/历史访问量创建全局 Fenwick array。
自动 RowHeightEstimate 的 owner 是 committed data generation。CommitRangePresentation 先用 DefaultRowHeight 构建新
generation 的高度索引;只有 range、snapshot、pins 与 staged containers 均准备成功、即将交换 presentation 时,才调用
ResetRowHeightEstimateForDataGeneration。这样失败或迟到的 generation 不污染当前高度状态;Source 换绑、Query/filter、分页和
Invalidated/collection Reset 成功后都从新 baseline 采样。首个有效 data row 的正常 measure 会更新 RowHeightEstimate 并 rebase
稀疏索引,显式 RowHeight 继续优先。重置为 O(1),不新增订阅、timer、CTS 或按总行数分配的结构。
测量修正以首个完整可见 entry key 与 intra-row offset 为锚。Query/PageRequest 成功后 vertical offset 归零;GroupExpansion 保持操作 header 的屏幕 Y;Invalidated 优先按首行 key 恢复,失败时 clamp 到最近合法 slot。
DataGrid 写入 vertical scrollbar 前仍把 extent travel clamp 到非负;AbstractScrollBar 再对所有有限值统一保证
Maximum >= Minimum。小于一个物理像素的 travel range 收敛到 Minimum,因此 Auto scrollbar 不抖动,也不会通过非法负
Maximum 触发 Avalonia RangeBase 校验异常。
8.5 Container recycle
回收时清理 DataContext、RowKey、DataIndex、WindowDataIndex、Slot、group metadata、selection/current/edit/hover、RowDetails、
cell sort/filter 状态和事件订阅。DataGridDisplayData 继续复用循环 displayed list 与 row/group pool;不能引入第二个
ItemsRepeater、VirtualizingStackPanel 或 scroll owner。
RowsPresenter.Children 只包含 visible 加一个 editing row 和一个 drag row 上限的 controls。Measure、Arrange、prepare 和 recycle 不 fetch、不等待、不分配 range buffer。嵌套滚动在内部边界前由 DataGrid 消费,到边界后交给外层 ScrollViewer。
8.6 Query 执行顺序
Source 使用固定顺序:
filter
-> stable Groups then Sorts
-> PageRequest over business rows
-> insert GroupHeader
-> apply GroupExpansion
-> flatten display entries
-> Range slice
Groups 形成领先 sort,同一 FieldId 不能重复出现在 Groups 与 Sorts。Source 省略 collapsed descendants;DataGrid 不为完整结果
建立全局 group-header 或 collapsed-slot table。GroupEntry.LeafCount 表示当前 PageRequest 内包含 collapsed descendants 的业务
行数,不是跨页 aggregate。
8.7 LocalSource
DataGridLocalSource 在 owner thread 捕获稳定引用快照和 sourceVersion;后台只处理快照的 int index buffer。Filter 不复制业务 对象,sort 依次比较 Groups、Sorts 和原 source ordinal,保证稳定输出。PageRequest 先切业务行;plain table 不建 group 对象; range fetch 只创建请求范围大小的 entry buffer。
空 Query、无分页、无分组的 plain table 直接按 request range 读取稳定 source snapshot,不创建全量 projection cache。只有 filter/sort/group/page 路径进入 index projection 与有限 LRU;因此百万行 plain 首屏仍只物化一个有界 range。
typed descriptor 提供 Func<T,TValue> getter、IComparer<TValue>、operator evaluator 与 scalar converter。比较循环不执行 LINQ、
反射、字符串 path、文化对象创建或 delegate 组合分配。projection cache 以 sourceVersion、Query、PageRequest 和 Expansion 的
相应 identity 分层,并受有限 LRU 管理。
8.8 Sort/filter visual delta
Query.Sorts 被归一为 FieldId 到 (direction, priority) 的小型映射,只更新 old/new delta 涉及的 column。Column.SortState 驱动
Header 的既有 sort pseudo-class 和 indicator。Cell 不长期订阅 Header;prepare/recycle 从 OwningColumn.SortState 初始化,Query
delta 只遍历已实现 cells。
Filter Flyout 从 Query.Filters 初始化,提交完整新 Query。候选 Filters 的集合变化只重新物化候选内容,不能绕过 Query owner。
8.9 Frame、列宽与水平 virtualization
- FrameCornerRadius 与 FrameContentClip 共同约束根内容裁剪;FrameBorderThickness 只由 frame border option 决定。
- 最后一行 divider 与 Frame 底边保持唯一绘制责任;Footer、底部分页或横向滚动条存在时恢复行 divider。
- 有限列 viewport 只进入 DataGrid-owned solver;Auto completion 与 star distribution 是独立阶段。
Auto/SizeToCells只消费已实现 cells 的正常 measure 结果;列宽发现不得 fetch 未显示 range 或扩大纵向 realized window。- 首屏需要稳定内容 extent 的场景由列
Pixel Width或MinWidth提供基线;水平 scrollbar 继续只投影真实 extent,不承担宽度策略。 - Star 列仍能吸收空间时 filler 为零;只有所有可调列达到 min/max 后才允许正 filler。
- 水平可见列、冻结列和 cell count 继续由 DataGridCellsPresenter 管理;纵向 commit 不全量重建 cells。
9. 资源、性能与 AOT 边界
9.1 热路径
- Scroll input 只更新/合并 DesiredViewport,handler 返回后由 coordinator 调度 request。
- 每次不同的 DesiredViewport 只替换一个 active scope;旧排队项不会作为无界 task 留在 semaphore 前等待。
- PointerMoved 只更新 drag session、ghost offset、target key 和必要自动滚动 intent。
- Measure/Arrange/prepare/recycle 只做 committed index lookup 与 container state 投影。
- Query no-op 不请求、不刷新 rows;sort state 没有 per-cell 长期订阅。
- Ready 模板不增加 visual node;prefetch 不触发布局。
- 同步 Source/cache hit 不发布瞬时 Loading/Refreshing;异步首次加载驱动 Spin,已有 snapshot 的异步刷新不启动自动遮罩。
9.2 复杂度与资源上限
| 路径 | 上限 |
|---|---|
| fixed-height offset lookup | O(1) |
| variable-height offset lookup | O(log M),M 由 cache/pin/显式状态约束 |
| realized row/group controls | visible + editing row + drag row |
| request concurrency | 默认最多 2 |
| active viewport scope | 每个 generation 1 个 |
| queued obsolete viewport work | 0;scope supersede 时从可发现队列移除并取消 |
| prefetch priority | 低于所有 visible 缺口,并绑定产生它的 committed scope |
| range size | Schema.MaximumRangeSize,最大 4096 |
| cache | 有限 block LRU,与 total 无关 |
| snapshot-expiry auto retry | 每个用户操作最多 1 次 |
所有 request、cache、pool、subscription 和 sparse state 必须在长时间往返滚动后达到稳态,不能随滚动次数或曾访问 row 数持续 增长。
9.3 AOT
- Query/Source public 类型只位于 DataGrid package,不引入 Core/Shared 反向依赖。
- LocalSource 使用静态泛型 descriptor,并把 typed getter 显式投影为 schema
DisplayAccessor;generatedIDataMemberAccessor可通过FromDataMember<TItem>复用。自动列只消费该 accessor 并生成 compiled binding,绝不把 FieldId 当作 CLR path。 - 没有
DisplayAccessor的远端 schema 字段只支持显式 Column/Binding;PropertyChangedName与 FieldId 相互独立,binding dispose/recycle 时解除INotifyPropertyChanged订阅。 - 不使用 PropertyInfo.GetValue、Expression.Compile、assembly scan、runtime registration、动态泛型构造或字符串 Binding。
- 不新增 linker root、trimming suppression 或反射 fallback。
- Source generator 输出只通过输入源和 generator 更新。
10. 维护不变量
维护 DataGrid 时不得破坏:
- Query、AppliedQuery、Source、Selection、CurrentRowKey 和 PresentationSnapshot 的单一 ownership。
- QueryRevision/DataGeneration 与 Source identity/snapshot 的迟到结果隔离。
- Result 精确切片、key 唯一性、totals 恒定和三索引域映射。
- DesiredViewport 与 CommittedViewport 隔离,layout 热路径零 I/O。
- 每个 generation 只有一个 active viewport scope;新 scope 先转移共享 block lease,再取消 orphaned visible/prefetch work。
- visible queue 优先于 prefetch queue;旧 prefetch 不能占据新 visible target 的排队顺序。
- Cache、pool、height metadata、selection 和 RowDetails 状态的有界或用户显式增长属性。
- Container reset 完整性,以及 realized/edit/drag/applied block 的 pin 生命周期。
- Pinned filter 的唯一目标和 Header -> Indicator -> Flyout relay 对称释放。
- Row reorder 的 DataGrid session、presenter ghost 与 Source mutation 职责分离。
- 列宽 solver 不依赖 RowsPresenter 可见性;filler 不掩盖 star 分配。
- ControlTheme key、Template Part、伪类、Token、Semantic Part 和 Ready 视觉优先级。
- Light/Dark、Browser/Desktop、SizeType、冻结列、RowDetails 和 nested scrolling 的一致语义。
- 文档、源码 public surface、Gallery、tests 与 generated LLMS 的一致性。
11. 测试与验证
11.1 Query、Source 与状态机
- Query validation、equality/hash、no-op、sort policy、schema capability 和 QueryChanged reentrancy。
- Range/result 边界、snapshot/totals consistency、entry payload、key uniqueness 与 index mapping。
- 旧 Query/Source/request 迟到、忽略 cancellation、snapshot expiry、Source replacement 和 detach。
- viewport scope 替换、共享 block lease 转移、orphaned request 协作取消、取消后 inflight 不可复用和 visible/prefetch 优先级。
- Initial error、refreshing error、stale invalidation、Query/page/group/scroll rollback。
- LocalSource typed filter/sort/group/page、stable tie-break、collection invalidation、cancellation 与 NativeAOT。
11.2 虚拟化与交互
- 0、1、短尾、分组、collapse 和 int 上限下 SlotCount/WindowDataCount/child index 精确。
- Source probe 证明 layout、prepare 和 recycle 的 fetch/wait 计数为 0。
- 固定/自动行高、RowDetails、extent、anchor、滚轮、惯性、thumb、keyboard 与 ScrollIntoView。
- 快速 viewport 跨多个 Dispatcher turn 时,旧排队/执行请求收到取消且最终 visible target 不等待过时队列;共享 block 不重复读取。
- cache miss 保留旧 rows,superseded cancellation 不进入 Error/rollback,成功只提交最终 viewport,失败恢复 offset/slot,且无 null/duplicate/flash reset。
- block pin、container reset、row/group pool 与 sparse heights 在 10,000 次滚动后不增长。
- selection/current 跨 range/page/recycle;全量选择不枚举数据;无 bulk capability 时不伪造全量命令成功。
- row reorder threshold、capture lost、session revalidation、key-relative target、source replacement 和异常清理。
- nested scrolling 在 DataGrid 边界处正确向外层 ScrollViewer 链接。
11.3 视觉、性能和发布
- Light/Dark、所有 SizeType、普通/分组 header、empty/data、Title/Footer、上下 pagination、冻结列、scrollbar 与 RowDetails。
- selected/sorted、hover/focus/disabled、Loading/Refreshing 与 rollback 的结构和截图基线。
- 100 万本地/逻辑远端行的 sort、首屏、连续滚动、快速跳转、cache、并发、offset lookup 与 allocation。
- DataGrid detail/recycle/offset/extent 测试保持或加强,不删除或放宽既有行为断言。
- 运行 DataGrid 专用测试、完整 solution tests、DataGrid performance state verifier、LLMS verify、AOT/trim verify、Gallery
NativeAOT publish/startup smoke 和
git diff --check。