mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-29 07:16:33 +08:00
perf(DataGrid): reduce column header clip allocation
Gallery qualification: DataGridShowCase declares 65 DataGrid instances and 210 column declarations, including multiple frozen-column examples with LeftFrozenColumnCount and RightFrozenColumnCount. The optimized path applies to realized DataGridColumnHeader instances that require clipping under frozen columns or header viewport constraints. Operation frequency: column header presenter arrange runs during initial layout, horizontal scrolling, resizing, column width changes, and grid relayout. Previously each clipped header arrange allocated a new RectangleGeometry. Avalonia source basis: Visual.Clip is a styled property registered with AffectsRender, and InvalidateVisual marks the visual dirty (.referenceprojects/Avalonia/src/Avalonia.Base/Visual.cs:52-53,141-149,418-421,446-500). RectangleGeometry.Rect participates in geometry invalidation (.referenceprojects/Avalonia/src/Avalonia.Base/Media/RectangleGeometry.cs:25-33; .referenceprojects/Avalonia/src/Avalonia.Base/Media/Geometry.cs:146-172). Benefit: repeated clipped column header arrange now reuses one RectangleGeometry after first use, reducing repeated clip geometry allocation from 1/clipped header arrange to 0 after first while preserving Clip clearing when clipping is no longer needed. Verification: dotnet build -c Release -f net10.0 tools/performances/AtomUI.Performance/AtomUI.Performance.csproj; dotnet run -c Release -f net10.0 --no-build --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- --verify-datagrid-states; dotnet run -c Release -f net10.0 --no-build --project tools/performances/AtomUI.Performance/AtomUI.Performance.csproj -- --suite datagrid --count 100; git diff --check.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# DataGrid 性能优化
|
||||
|
||||
> 路线图位置:[`../desktop-controls-optimization-roadmap.md`](../desktop-controls-optimization-roadmap.md) Phase F / Tier 4
|
||||
> 状态:T4.1 cell header-state binding + row/row group header pointer handler + core input handler + rows presenter scroll gesture / clip geometry + row gridline / details presenter / cell clip geometry cleanup partial done;T4.2 column filter flyout lifecycle + filter indicator binding + column/group header pointer handler cleanup partial done;T4.3 special column detach lifecycle + row expander details binding partial done;T4.4 column reorder drag indicator render partial done;T4.5 row details presenter measure registration partial done;DataGrid core / row / virtualization 仍待后续专项。
|
||||
> 状态:T4.1 cell header-state binding + row/row group header pointer handler + core input handler + rows presenter scroll gesture / clip geometry + row gridline / details presenter / cell clip geometry cleanup partial done;T4.2 column filter flyout lifecycle + filter indicator binding + column/group header pointer handler + column header clip cleanup partial done;T4.3 special column detach lifecycle + row expander details binding partial done;T4.4 column reorder drag indicator render partial done;T4.5 row details presenter measure registration partial done;DataGrid core / row / virtualization 仍待后续专项。
|
||||
|
||||
---
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- filter indicator 可见性不再为每个列头创建 3 路 `MultiBinding` + converter,改为列头 DirectProperty + `{TemplateBinding}`。
|
||||
- column header hover 不再为每个 realized `DataGridColumnHeader` 注册本地 `PointerEntered` / `PointerExited` handler,改走 Avalonia 的 virtual override 路径。
|
||||
- column header press/release/move 不再为每个 realized `DataGridColumnHeader` 注册本地 handler;内部 resize/reorder/sort 逻辑和 `DataGridColumn.HeaderPointerPressed/Released` 转发都改为 class handler。
|
||||
- `DataGridColumnHeader` 在 frozen-column 裁剪路径不再每次 header presenter arrange 新建 clip `RectangleGeometry`,改为 header 级复用;不需要裁剪时仍清空 `Clip`。
|
||||
- column group header press/release 不再为每个 realized `DataGridColumnGroupHeader` 注册本地 forwarding lambda,改为 class handler 转发到 `DataGridColumnGroupItem.HeaderPointerPressed/Released`。
|
||||
- row header press 不再为每个 realized `DataGridRowHeader` 注册本地 `PointerPressed` handler,改为 class handler,同时保留 focus / selection / current slot 行为。
|
||||
- row group header press 不再为每个 realized `DataGridRowGroupHeader` 注册本地 `PointerPressed` lambda,改为 class handler,同时保留 current slot 行为。
|
||||
@@ -96,6 +97,7 @@ Gallery source:`controlgallery/AtomUIGallery/ShowCases/Views/DataDisplay/DataG
|
||||
| Runtime `Filters` collection visibility refresh | binding-path dependent | explicit `CollectionChanged` refresh | correctness path added | 正确性修复;add/clear filter items 均验证 |
|
||||
| Column header local hover routed handlers | 2 per realized header | 0 per realized header | 100.00% removed | 结构/分配优化;hover 语义保留在 `OnPointerEntered` / `OnPointerExited` override |
|
||||
| Column header local press/release/move handlers | 5 per standard header | 0 per standard header | 100.00% removed | 结构/分配优化;internal handler + public column event forwarding 均迁到 class handler |
|
||||
| Column header clip geometry allocation | 1 new `RectangleGeometry` per clipped header arrange | 0 new `RectangleGeometry` per clipped header arrange after first | 100.00% repeated-arrange allocation removed | 结构/分配优化;frozen-column header clip instance 复用、Rect 更新和 clear 已验证 |
|
||||
| Column group header forwarding handlers | 2 per group header | 0 per group header | 100.00% removed | 结构/分配优化;Gallery group header 示例有 4 个 group items |
|
||||
| Row header local press handler | 1 per row header | 0 per row header | 100.00% removed | 结构/分配优化;行头点击 selection / current slot 语义已验证 |
|
||||
| Row group header local press handler | 1 per row group header | 0 per row group header | 100.00% removed | 结构/分配优化;分组头点击 current slot 语义已验证 |
|
||||
@@ -569,6 +571,32 @@ Gallery `Grouping table head` 示例声明 4 个 `DataGridColumnGroupItem`,页
|
||||
| DataGrid.RowGroups | 10.897 | 2998.9 | 321.0 | 1.0 | smoke 通过;timing 只作异常检查 |
|
||||
| DataGrid.GalleryShape | 45.449 | 12482.3 | 1260.0 | 5.0 | smoke 通过;timing 只作异常检查 |
|
||||
|
||||
### 2.20 Column header clip geometry allocation
|
||||
|
||||
本轮优化点:`DataGridColumnHeadersPresenter.EnsureColumnHeaderClip()` 原先只要 scrolling column header 被 frozen columns 或右侧边界裁剪,每次 header presenter arrange 都会创建新的 `RectangleGeometry` 并赋给 `DataGridColumnHeader.Clip`。真实 Gallery `DataGridShowCase` 声明 65 个 `DataGrid`、210 个 column declarations,多个 frozen-column 示例设置 `LeftFrozenColumnCount` / `RightFrozenColumnCount`;水平滚动、窗口尺寸变化、列宽变化和 grid relayout 都会触发 header presenter arrange。
|
||||
|
||||
修复后 `DataGridColumnHeader` 持有一个 `_clipGeometry`,首次需要裁剪时创建,后续只更新 `Rect`;裁剪不再需要或列头进入 frozen 区时仍清空 `Clip`。由于 `Visual.Clip` 换引用会触发 `AffectsRender`,而复用 geometry 只更新 `RectangleGeometry.Rect`,本轮在 `Rect` 变化时显式 `InvalidateVisual()`,保持裁剪刷新语义。
|
||||
|
||||
| 场景 | baseline | optimized | 改善 | 结论 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Column header clip allocation / repeated arrange | 1 new `RectangleGeometry` / clipped header arrange | 0 new `RectangleGeometry` / clipped header arrange after first | 100.00% repeated allocation removed | 有效;每 header 生命周期内从 `N` 次分配降到 `1` 次 |
|
||||
| 6-column frozen-header verification pass | 1 new `RectangleGeometry` / clipped scrolling header pass | 0 new `RectangleGeometry` / clipped scrolling header pass after first | 100.00% repeated allocation removed | 结构收益;按验证场景单个 clipped scrolling header 估算 |
|
||||
| Clip geometry instance | replaced every arrange | reused across repeated arrange and horizontal offset change | reuse verified | `--verify-datagrid-states` 覆盖 |
|
||||
| Clip clear path | assign `Clip = null` | `ClearClipGeometry()` clears `Clip` while keeping reusable cache | 行为保留 | frozen clipping 取消后 `Clip` 清空已验证 |
|
||||
|
||||
当前工作区 smoke:
|
||||
|
||||
| Scenario | ms/item | KB/item | Visual/root | Logical/root | 结论 |
|
||||
| --- | ---: | ---: | ---: | ---: | --- |
|
||||
| DataGrid.Basic | 16.045 | 2964.7 | 305.0 | 1.0 | smoke 通过;timing 只作异常检查 |
|
||||
| DataGrid.Filter.Menu.Closed | 10.194 | 2599.2 | 267.0 | 1.0 | smoke 通过;timing 只作异常检查 |
|
||||
| DataGrid.Filter.Tree.Closed | 9.336 | 2596.4 | 267.0 | 1.0 | smoke 通过;timing 只作异常检查 |
|
||||
| DataGrid.RowHeaders | 11.296 | 3122.9 | 336.0 | 1.0 | smoke 通过;timing 只作异常检查 |
|
||||
| DataGrid.RowDetails.Collapsed | 11.803 | 3041.9 | 320.0 | 1.0 | smoke 通过;timing 只作异常检查 |
|
||||
| DataGrid.GroupHeaders | 9.993 | 2592.3 | 266.0 | 1.0 | smoke 通过;timing 只作异常检查 |
|
||||
| DataGrid.RowGroups | 11.546 | 2998.7 | 321.0 | 1.0 | smoke 通过;timing 只作异常检查 |
|
||||
| DataGrid.GalleryShape | 47.777 | 12479.6 | 1260.0 | 5.0 | smoke 通过;timing 只作异常检查 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 验证
|
||||
@@ -585,7 +613,7 @@ dotnet run -c Release -f net10.0 --no-build \
|
||||
--verify-datagrid-states
|
||||
```
|
||||
|
||||
结果:`DataGrid state verification passed.` 覆盖:无 filter items 不创建 shell、关闭态 filter content lazy、运行时 add/clear filter items 后 indicator 可见性和 flyout shell 同步、realized column header 不再注册本地 hover / press / release / move routed handlers、`HeaderPointerPressed` class handler 转发 exactly once 且 sender 保持为 `DataGridColumn`、realized column group header 不再注册本地 press / release forwarding handlers、group `HeaderPointerPressed` class handler 转发 exactly once 且 sender 保持为 `DataGridColumnGroupItem`、realized row header 不再注册本地 press handler 且左键点击仍选中行并更新 `CurrentSlot`、realized row group header 不再注册本地 press handler 且左键点击仍更新 `CurrentSlot`、realized DataGrid core 不再注册本地 `KeyDown` / `KeyUp` / `GotFocus` / `LostFocus` handlers,且 `GotFocus` 更新 `ContainsFocus`、`Down` key 仍推动 current slot、realized rows presenter 不再注册本地 `ScrollGesture` handler,且滚动手势仍 handled 并更新 `VerticalOffset`、rows presenter clip `RectangleGeometry` 跨重复 arrange 和尺寸变化复用且 `Rect` 会更新、row bottom grid-line clip `RectangleGeometry` 跨重复 arrange 和水平偏移变化复用且 `Rect` 会更新、cell clip `RectangleGeometry` 跨 repeated cells presenter arrange 和水平偏移变化复用且 clipping 取消后清空 `Clip`、details presenter clip `RectangleGeometry` 跨重复 arrange 和水平偏移变化复用且 frozen 模式清空 `Clip`、分组行头内 row header 的 owner/template 顺序不再空引用、DataGridCell 跟随 header sort / reorder state 且 detach 后释放订阅、special columns 在 `Columns.Clear()` 后释放 cached owning grid、column reorder drag indicator 复用 cached render pen 且 foreground 变化后重建、DetailsPresenter `ContentHeight` 改变后触发 measure invalidation、RowExpander checked/details visibility 双向同步和 detach 释放。
|
||||
结果:`DataGrid state verification passed.` 覆盖:无 filter items 不创建 shell、关闭态 filter content lazy、运行时 add/clear filter items 后 indicator 可见性和 flyout shell 同步、realized column header 不再注册本地 hover / press / release / move routed handlers、`HeaderPointerPressed` class handler 转发 exactly once 且 sender 保持为 `DataGridColumn`、realized column group header 不再注册本地 press / release forwarding handlers、group `HeaderPointerPressed` class handler 转发 exactly once 且 sender 保持为 `DataGridColumnGroupItem`、realized row header 不再注册本地 press handler 且左键点击仍选中行并更新 `CurrentSlot`、realized row group header 不再注册本地 press handler 且左键点击仍更新 `CurrentSlot`、realized DataGrid core 不再注册本地 `KeyDown` / `KeyUp` / `GotFocus` / `LostFocus` handlers,且 `GotFocus` 更新 `ContainsFocus`、`Down` key 仍推动 current slot、realized rows presenter 不再注册本地 `ScrollGesture` handler,且滚动手势仍 handled 并更新 `VerticalOffset`、rows presenter clip `RectangleGeometry` 跨重复 arrange 和尺寸变化复用且 `Rect` 会更新、row bottom grid-line clip `RectangleGeometry` 跨重复 arrange 和水平偏移变化复用且 `Rect` 会更新、cell clip `RectangleGeometry` 跨 repeated cells presenter arrange 和水平偏移变化复用且 clipping 取消后清空 `Clip`、column header clip `RectangleGeometry` 跨 repeated header presenter arrange 和水平偏移变化复用且 clipping 取消后清空 `Clip`、details presenter clip `RectangleGeometry` 跨重复 arrange 和水平偏移变化复用且 frozen 模式清空 `Clip`、分组行头内 row header 的 owner/template 顺序不再空引用、DataGridCell 跟随 header sort / reorder state 且 detach 后释放订阅、special columns 在 `Columns.Clear()` 后释放 cached owning grid、column reorder drag indicator 复用 cached render pen 且 foreground 变化后重建、DetailsPresenter `ContentHeight` 改变后触发 measure invalidation、RowExpander checked/details visibility 双向同步和 detach 释放。
|
||||
|
||||
```bash
|
||||
dotnet run -c Release -f net10.0 --no-build \
|
||||
@@ -597,11 +625,11 @@ dotnet run -c Release -f net10.0 --no-build \
|
||||
|
||||
| Scenario | ms/item | KB/item | Visual/root | Logical/root |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| DataGrid.Basic | 15.326 | 2964.9 | 305.0 | 1.0 |
|
||||
| DataGrid.Filter.Menu.Closed | 10.343 | 2600.7 | 267.0 | 1.0 |
|
||||
| DataGrid.Filter.Tree.Closed | 8.870 | 2596.6 | 267.0 | 1.0 |
|
||||
| DataGrid.RowHeaders | 10.448 | 3123.2 | 336.0 | 1.0 |
|
||||
| DataGrid.RowDetails.Collapsed | 10.622 | 3042.1 | 320.0 | 1.0 |
|
||||
| DataGrid.GroupHeaders | 10.193 | 2592.2 | 266.0 | 1.0 |
|
||||
| DataGrid.RowGroups | 10.897 | 2998.9 | 321.0 | 1.0 |
|
||||
| DataGrid.GalleryShape | 45.449 | 12482.3 | 1260.0 | 5.0 |
|
||||
| DataGrid.Basic | 16.045 | 2964.7 | 305.0 | 1.0 |
|
||||
| DataGrid.Filter.Menu.Closed | 10.194 | 2599.2 | 267.0 | 1.0 |
|
||||
| DataGrid.Filter.Tree.Closed | 9.336 | 2596.4 | 267.0 | 1.0 |
|
||||
| DataGrid.RowHeaders | 11.296 | 3122.9 | 336.0 | 1.0 |
|
||||
| DataGrid.RowDetails.Collapsed | 11.803 | 3041.9 | 320.0 | 1.0 |
|
||||
| DataGrid.GroupHeaders | 9.993 | 2592.3 | 266.0 | 1.0 |
|
||||
| DataGrid.RowGroups | 11.546 | 2998.7 | 321.0 | 1.0 |
|
||||
| DataGrid.GalleryShape | 47.777 | 12479.6 | 1260.0 | 5.0 |
|
||||
|
||||
@@ -164,7 +164,7 @@ Avalonia 12 控件库研发的成本模型与高频踩雷点(含 `path:line`
|
||||
| Data Display | Carousel | Done | [Carousel](Carousel/README.md);nav/progress/PageTransition 已按需创建,真实 Gallery alloc `6113.86KB -> 5213.10KB`,cold `164.35ms -> 149.05ms`,repeated timing 基本持平 |
|
||||
| Data Display | Collapse | Done | [Collapse](Collapse/README.md);addon、no-arrow、closed content 已按需创建,Gallery repeated mean 提升约 `16.03%` |
|
||||
| Data Display | Descriptions | Done | [Descriptions](Descriptions/README.md);`DescriptionsShowCase` repeated mean `75.84ms -> 68.64ms`,visuals `579 -> 571`;binding/collection/window 订阅生命周期已补齐验证 |
|
||||
| Data Display | DataGrid | Partial | [DataGrid](DataGrid/README.md);filter flyout 内容延迟到首次打开,关闭态过滤菜单项 `7 -> 0`;filter indicator 可见性由 `MultiBinding` 收敛到 `TemplateBinding`;column/group/row header 本地 routed handlers 已收敛(column header hover `2/header -> 0/header`,press/release/move + forwarding `5/header -> 0/header`,group header forwarding `2/header -> 0/header`,row header press `1/header -> 0/header`,row group header press `1/header -> 0/header`);DataGrid core input handlers `4/grid -> 0/grid`;rows presenter scroll gesture handler `1/presenter -> 0/presenter`;rows presenter clip geometry repeated arrange allocation `1 -> 0` after first;row bottom grid-line clip repeated arrange allocation `1/row -> 0/row` after first;details presenter clip repeated arrange allocation `1/details -> 0/details` after first;cell clip repeated arrange allocation `1/cell -> 0/cell` after first;DataGridCell header-state binding converter 闭包已移除;special columns 在 `Columns.Clear()` 后释放 grid 事件链;列重排拖拽 indicator 复用 dashed `Pen`;DetailsPresenter measure registration 与 RowExpander details binding cleanup 已完成;分组行头内 row header owner/template 顺序空引用已修复 |
|
||||
| Data Display | DataGrid | Partial | [DataGrid](DataGrid/README.md);filter flyout 内容延迟到首次打开,关闭态过滤菜单项 `7 -> 0`;filter indicator 可见性由 `MultiBinding` 收敛到 `TemplateBinding`;column/group/row header 本地 routed handlers 已收敛(column header hover `2/header -> 0/header`,press/release/move + forwarding `5/header -> 0/header`,group header forwarding `2/header -> 0/header`,row header press `1/header -> 0/header`,row group header press `1/header -> 0/header`);column header clip repeated arrange allocation `1/header -> 0/header` after first;DataGrid core input handlers `4/grid -> 0/grid`;rows presenter scroll gesture handler `1/presenter -> 0/presenter`;rows presenter clip geometry repeated arrange allocation `1 -> 0` after first;row bottom grid-line clip repeated arrange allocation `1/row -> 0/row` after first;details presenter clip repeated arrange allocation `1/details -> 0/details` after first;cell clip repeated arrange allocation `1/cell -> 0/cell` after first;DataGridCell header-state binding converter 闭包已移除;special columns 在 `Columns.Clear()` 后释放 grid 事件链;列重排拖拽 indicator 复用 dashed `Pen`;DetailsPresenter measure registration 与 RowExpander details binding cleanup 已完成;分组行头内 row header owner/template 顺序空引用已修复 |
|
||||
| Data Display | Expander | Done | [Expander](Expander/README.md);closed content/addon/no-arrow slot 已按需创建,Gallery repeated mean 提升约 `18.11%` |
|
||||
| Data Display | Empty | Done | [Empty](Empty/README.md);低风险正确性修复与热路径收敛,Gallery repeated mean 小幅改善,visual tree 不变 |
|
||||
| Data Display | GroupBox | Done | [GroupBox](GroupBox/README.md);header icon presenter 已按需创建,`GroupBoxShowCase` visuals `151 -> 143`,repeated mean `27.79ms -> 26.16ms` |
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
|
||||
| 控件 | 状态 | 跨包依赖 |
|
||||
| --- | --- | --- |
|
||||
| DataGrid | Partial | Primitives.ArrowDecoratedBox, Buttons.Button, TextBlock, Pagination, PopupConfirm, ScrollViewer, Spin, Tooltip.ToolTip, TreeView.TreeViewItemTheme;cell header-state binding / filter flyout lifecycle / filter indicator binding / column/group/row/row group header pointer handler cleanup / core input handler cleanup / rows presenter scroll gesture + clip geometry cleanup / row bottom gridline clip cleanup / details presenter clip cleanup / cell clip cleanup / special column detach lifecycle / column reorder indicator pen cache / details presenter measure registration / row expander details binding 已完成,详见 [DataGrid](DataGrid/README.md) |
|
||||
| DataGrid | Partial | Primitives.ArrowDecoratedBox, Buttons.Button, TextBlock, Pagination, PopupConfirm, ScrollViewer, Spin, Tooltip.ToolTip, TreeView.TreeViewItemTheme;cell header-state binding / filter flyout lifecycle / filter indicator binding / column/group/row/row group header pointer handler cleanup / column header clip cleanup / core input handler cleanup / rows presenter scroll gesture + clip geometry cleanup / row bottom gridline clip cleanup / details presenter clip cleanup / cell clip cleanup / special column detach lifecycle / column reorder indicator pen cache / details presenter measure registration / row expander details binding 已完成,详见 [DataGrid](DataGrid/README.md) |
|
||||
| DataGridRow / DataGridCell / DataGridColumnHeader 等 ~40 内部子控件 | Pending | 同上 |
|
||||
|
||||
DataGrid 是最重的复合控件,独立子表才是合理的优化粒度。
|
||||
@@ -240,7 +240,7 @@ DataGrid 是单文件最复杂的控件之一(~110 个公开类型)。依赖
|
||||
| # | 子模块 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| T4.1 | DataGrid 核心(DataGrid / DataGridRow / DataGridCell / Presenter 系列) | Partial:DataGridCell header sort / reorder state binding converter 闭包已移除,cell clip repeated arrange 分配已收敛;DataGridRowHeader press 本地 handler 已迁到 class handler,且 owner/template 顺序空引用已修复;DataGridRowGroupHeader press 本地 handler 已迁到 class handler;DataGrid core input handlers 已迁到 virtual override;DataGridRowsPresenter scroll gesture handler 已迁到 class handler,presenter clip geometry repeated arrange 分配已收敛;DataGridRow bottom gridline clip repeated arrange 分配已收敛;DataGridDetailsPresenter clip repeated arrange 分配已收敛;DetailsPresenter measure registration 已收敛;DataGrid / DataGridRow / 其他 Presenter / virtualization 仍待专项 |
|
||||
| T4.2 | 列模型(DataGridColumn / DataGridColumnHeader / FilterFlyout / SortIndicator) | Partial:filter flyout closed-state 内容已延迟;filter indicator visibility binding 已收敛;column header hover / press / release / move 本地 handler 已清理;column group header press / release forwarding handler 已清理;其余 sort / resize / reorder 仍待专项 |
|
||||
| T4.2 | 列模型(DataGridColumn / DataGridColumnHeader / FilterFlyout / SortIndicator) | Partial:filter flyout closed-state 内容已延迟;filter indicator visibility binding 已收敛;column header hover / press / release / move 本地 handler 已清理;column header clip repeated arrange 分配已收敛;column group header press / release forwarding handler 已清理;其余 sort / resize / reorder 仍待专项 |
|
||||
| T4.3 | 行展开 / 详情(RowExpander / DetailsPresenter) | Partial:detail / reorder / selection / checkbox / operation columns 的 `Columns.Clear()` detach 事件链已释放;DetailsPresenter `ContentHeight` measure registration 已移到 static constructor;DetailsPresenter clip repeated arrange 分配已收敛且 frozen clear 已验证;RowExpander details visibility binding 已从 `RelayBind` 收敛到 direct observable + detach 释放;其余 row details layout 仍需与 T4.1 协同 |
|
||||
| T4.4 | 行重排 / 选择(RowReorder / SelectionColumn) | Partial:column reorder dragging-over indicator 已缓存 dashed `Pen`;拖拽全局订阅仍是 Lifecycle Pairing 重点 |
|
||||
| T4.5 | 分组 / 过滤(CollectionView / FilterDescription / GroupDescription) | 数据层成本,不在主题路径上但是真热点 |
|
||||
|
||||
@@ -288,6 +288,7 @@ internal partial class DataGridColumnHeader : ContentControl
|
||||
private bool _areHandlersSuspended;
|
||||
private bool _desiredSeparatorVisibility = true;
|
||||
private INotifyCollectionChanged? _subscribedFilterItems;
|
||||
private RectangleGeometry? _clipGeometry;
|
||||
private static Lazy<Cursor> ResizeCursor = new (() => new Cursor(StandardCursorType.SizeWestEast));
|
||||
|
||||
static DataGridColumnHeader()
|
||||
@@ -329,6 +330,35 @@ internal partial class DataGridColumnHeader : ContentControl
|
||||
SetValueNoCallback(IsSeparatorsVisibleProperty, newVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateClipGeometry(Rect clipRect)
|
||||
{
|
||||
if (_clipGeometry is null)
|
||||
{
|
||||
_clipGeometry = new RectangleGeometry
|
||||
{
|
||||
Rect = clipRect
|
||||
};
|
||||
}
|
||||
else if (!_clipGeometry.Rect.Equals(clipRect))
|
||||
{
|
||||
_clipGeometry.Rect = clipRect;
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(Clip, _clipGeometry))
|
||||
{
|
||||
Clip = _clipGeometry;
|
||||
}
|
||||
}
|
||||
|
||||
internal void ClearClipGeometry()
|
||||
{
|
||||
if (Clip is not null)
|
||||
{
|
||||
Clip = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal void HandleMouseLeftButtonUpClick(KeyModifiers keyModifiers, ref bool handled)
|
||||
{
|
||||
|
||||
@@ -132,7 +132,7 @@ public sealed class DataGridColumnHeadersPresenter : Panel, IChildIndexProvider
|
||||
if (dataGridColumn.IsLeftFrozen)
|
||||
{
|
||||
columnHeader.Arrange(new Rect(frozenLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height));
|
||||
columnHeader.Clip = null; // The layout system could have clipped this because it's not aware of our render transform
|
||||
columnHeader.ClearClipGeometry(); // The layout system could have clipped this because it's not aware of our render transform
|
||||
if (DragColumn == dataGridColumn && DragIndicator != null)
|
||||
{
|
||||
dragIndicatorLeftEdge = frozenLeftEdge + DragIndicatorOffset;
|
||||
@@ -174,7 +174,7 @@ public sealed class DataGridColumnHeadersPresenter : Panel, IChildIndexProvider
|
||||
{
|
||||
frozenRightEdge -= dataGridColumn.ActualWidth;
|
||||
columnHeader.Arrange(new Rect(frozenRightEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height));
|
||||
columnHeader.Clip = null; // The layout system could have clipped this because it's not aware of our render transform
|
||||
columnHeader.ClearClipGeometry(); // The layout system could have clipped this because it's not aware of our render transform
|
||||
columnHeader.IsFrozen = true;
|
||||
columnHeader.FrozenShadowPosition = FrozenColumnShadowPosition.Left;
|
||||
var horizontalScrollBarVisible = OwningGrid.HorizontalScrollBar?.IsVisible ?? false;
|
||||
@@ -241,30 +241,28 @@ public sealed class DataGridColumnHeadersPresenter : Panel, IChildIndexProvider
|
||||
frozenLeftEdge > columnHeaderLeftEdge &&
|
||||
columnHeaderRightEdge > frozenRightEdge)
|
||||
{
|
||||
RectangleGeometry rg = new RectangleGeometry();
|
||||
double xClip = Math.Round(Math.Min(width, frozenLeftEdge - columnHeaderLeftEdge));
|
||||
rg.Rect = new Rect(xClip, 0, Math.Max(0, width - (frozenLeftEdge - columnHeaderLeftEdge) - (columnHeaderRightEdge - frozenRightEdge)), height);
|
||||
columnHeader.Clip = rg;
|
||||
columnHeader.UpdateClipGeometry(new Rect(
|
||||
xClip,
|
||||
0,
|
||||
Math.Max(0, width - (frozenLeftEdge - columnHeaderLeftEdge) - (columnHeaderRightEdge - frozenRightEdge)),
|
||||
height));
|
||||
}
|
||||
else if (columnHeader.OwningColumn != null && !columnHeader.OwningColumn.IsFrozen &&
|
||||
frozenLeftEdge > columnHeaderLeftEdge)
|
||||
{
|
||||
RectangleGeometry rg = new RectangleGeometry();
|
||||
double xClip = Math.Round(Math.Min(width, frozenLeftEdge - columnHeaderLeftEdge));
|
||||
rg.Rect = new Rect(xClip, 0, Math.Max(0, width - xClip), height);
|
||||
columnHeader.Clip = rg;
|
||||
columnHeader.UpdateClipGeometry(new Rect(xClip, 0, Math.Max(0, width - xClip), height));
|
||||
}
|
||||
else if (columnHeader.OwningColumn != null && !columnHeader.OwningColumn.IsFrozen &&
|
||||
columnHeaderRightEdge > frozenRightEdge)
|
||||
{
|
||||
RectangleGeometry rg = new RectangleGeometry();
|
||||
double xClip = Math.Round(Math.Min(width, columnHeaderRightEdge - frozenRightEdge));
|
||||
rg.Rect = new Rect(0, 0, Math.Max(0, width - xClip), height);
|
||||
columnHeader.Clip = rg;
|
||||
columnHeader.UpdateClipGeometry(new Rect(0, 0, Math.Max(0, width - xClip), height));
|
||||
}
|
||||
else
|
||||
{
|
||||
columnHeader.Clip = null;
|
||||
columnHeader.ClearClipGeometry();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ internal static partial class Program
|
||||
VerifyDataGridRowsPresenterReusesClipGeometry(failures);
|
||||
VerifyDataGridRowReusesBottomGridLineClipGeometry(failures);
|
||||
VerifyDataGridCellsPresenterReusesCellClipGeometry(failures);
|
||||
VerifyDataGridColumnHeadersPresenterReusesHeaderClipGeometry(failures);
|
||||
VerifyDataGridCellHeaderStateBindings(failures);
|
||||
VerifyDataGridSpecialColumnsReleaseGridSubscriptionsOnClear(failures);
|
||||
VerifyDataGridColumnDragIndicatorCachesPen(failures);
|
||||
@@ -645,6 +646,91 @@ internal static partial class Program
|
||||
failures);
|
||||
}
|
||||
|
||||
private static void VerifyDataGridColumnHeadersPresenterReusesHeaderClipGeometry(ICollection<string> failures)
|
||||
{
|
||||
var grid = CreateDataGridShell(8);
|
||||
grid.Width = 260;
|
||||
grid.LeftFrozenColumnCount = 1;
|
||||
grid.RightFrozenColumnCount = 1;
|
||||
var columns = new List<DataGridColumn>();
|
||||
for (var i = 0; i < 6; i++)
|
||||
{
|
||||
var column = new DataGridTextColumn
|
||||
{
|
||||
Header = $"Column {i + 1}",
|
||||
Width = new DataGridLength(120),
|
||||
Binding = new Binding(GetDataGridBindingPath(i))
|
||||
};
|
||||
columns.Add(column);
|
||||
grid.Columns.Add(column);
|
||||
}
|
||||
|
||||
using var realized = RealizeControl(grid);
|
||||
|
||||
Expect(UpdateDataGridHorizontalOffset(grid, 50),
|
||||
"DataGrid should accept a reflected horizontal offset update for column header clip verification.",
|
||||
failures);
|
||||
RefreshLayout(realized.Window);
|
||||
|
||||
var header = GetDataGridColumnHeader(columns[1], failures);
|
||||
if (header is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var firstClip = header.Clip as RectangleGeometry;
|
||||
Expect(firstClip is not null && firstClip.Rect.X > 0,
|
||||
$"DataGrid scrolling column header should be clipped by a RectangleGeometry after horizontal offset. Actual: {header.Clip?.GetType().Name ?? "null"} {firstClip?.Rect}.",
|
||||
failures);
|
||||
if (firstClip is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var firstRect = firstClip.Rect;
|
||||
Expect(firstRect.Height > 0,
|
||||
$"DataGrid scrolling column header clip should have usable bounds. Actual: {firstRect}.",
|
||||
failures);
|
||||
|
||||
foreach (var presenter in GetDataGridColumnHeadersPresenters(grid))
|
||||
{
|
||||
presenter.InvalidateArrange();
|
||||
}
|
||||
RefreshLayout(realized.Window);
|
||||
|
||||
var secondClip = header.Clip as RectangleGeometry;
|
||||
Expect(ReferenceEquals(firstClip, secondClip),
|
||||
"DataGrid column header should reuse its clip RectangleGeometry across repeated header presenter arrange passes.",
|
||||
failures);
|
||||
|
||||
Expect(UpdateDataGridHorizontalOffset(grid, 80),
|
||||
"DataGrid should accept a second horizontal offset update for column header clip verification.",
|
||||
failures);
|
||||
RefreshLayout(realized.Window);
|
||||
|
||||
var scrolledClip = header.Clip as RectangleGeometry;
|
||||
Expect(ReferenceEquals(firstClip, scrolledClip),
|
||||
"DataGrid column header should keep reusing the same clip RectangleGeometry after horizontal offset changes.",
|
||||
failures);
|
||||
Expect(scrolledClip is not null && !scrolledClip.Rect.Equals(firstRect),
|
||||
$"DataGrid reused column header clip geometry should update Rect after horizontal offset changes. Before: {firstRect}, after: {scrolledClip?.Rect}.",
|
||||
failures);
|
||||
|
||||
grid.LeftFrozenColumnCount = 0;
|
||||
grid.RightFrozenColumnCount = 0;
|
||||
grid.Width = 900;
|
||||
UpdateDataGridHorizontalOffset(grid, 0);
|
||||
foreach (var presenter in GetDataGridColumnHeadersPresenters(grid))
|
||||
{
|
||||
presenter.InvalidateArrange();
|
||||
}
|
||||
RefreshLayout(realized.Window);
|
||||
|
||||
Expect(header.Clip is null,
|
||||
"DataGrid column header should clear Clip when frozen-column clipping is no longer needed.",
|
||||
failures);
|
||||
}
|
||||
|
||||
private static void VerifyDataGridCellHeaderStateBindings(ICollection<string> failures)
|
||||
{
|
||||
var grid = CreateDataGridShell(4);
|
||||
@@ -1004,6 +1090,13 @@ internal static partial class Program
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<DataGridColumnHeadersPresenter> GetDataGridColumnHeadersPresenters(Control root)
|
||||
{
|
||||
return root.GetSelfAndVisualDescendants()
|
||||
.OfType<DataGridColumnHeadersPresenter>()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<DataGridDetailsPresenter> GetDataGridDetailsPresenters(Control root)
|
||||
{
|
||||
return root.GetSelfAndVisualDescendants()
|
||||
|
||||
Reference in New Issue
Block a user