WPF DataGrid 选择机制:SelectedCells 与 SelectionUnit
结论
SelectedCells 是 DataGrid 内部唯一的单元格级选择存储(DataGrid.cs:4094 返回内部字段 _selectedCells),而 SelectionUnit 决定的是”允许以什么粒度产生选择”以及”选择是否与行(SelectedItems)联动”。一句话概括:SelectionUnit 是规则,SelectedCells 是状态。
SelectedCells 的本质
- 类型为
IList<DataGridCellInfo>,底层是SelectedCellsCollection : VirtualizedCellInfoCollection(DataGrid.cs:87)。它是虚拟化的:不需要DataGridCell容器真实存在(行滚出可视区被回收后选择仍在),元素是(Item, Column)对。 - 集合中的
DataGridCellInfo可能失效(IsValid,DataGridCellInfo.cs:190)——指向已被移除的 Item 或 Column 时为 false,遍历前应检查。 - 所有对它的合法修改都包在
using (UpdateSelectedCells())作用域内(DataGrid.cs:5228):变更先记入_pendingSelectedCells/_pendingUnselectedCells,作用域 Dispose 时统一执行UpdateIsSelected(把可见 cell 容器的IsSelected依赖属性同步)并触发公开事件SelectedCellsChanged(DataGrid.cs:4228)。 - 反向通路:cell 的
IsSelected变化经DataGridCell.OnIsSelectedChanged(DataGridCell.cs:651)回调DataGrid.CellIsSelectedChanged(DataGrid.cs:4665)写回集合。
SelectionUnit 三种取值的行为对照
| 行为 | FullRow(默认) | Cell | CellOrRowHeader |
|---|---|---|---|
| 点击单元格 | 选中整行 MakeFullRowSelection | 选中该单元格 MakeCellSelection | 选中该单元格 |
| 点击行头 | 选中整行 | 无效果 | 选中整行+填充该行全部 Cells |
| Ctrl+A / SelectAll | SelectAll() 选行 | SelectAllCells() 选全部单元格 | SelectAll() 选行 |
| 代码直改 SelectedCells | ❌ 抛 InvalidOperationException | ✅ 允许 | ✅ 允许(联动行) |
| SelectedItems(行选择) | 用户输入的有效目标 | 恒为空(CanSelectRows=false) | 有效目标 |
| SelectedCells ↔ SelectedItems | 只读镜像:选行后自动填充整行 Cells | 唯一选择状态,与行无关 | 双向联动:取消任一 Cell 会取消其所在行 |
关键源码行为:
- FullRow 下
SelectedCells是SelectedItems的只读镜像。用户选行 →OnSelectionChanged(DataGrid.cs:4495)→UpdateSelectionOfCellsInRow(DataGrid.cs:4618)用_selectedCells.AddRegion(rowIndex, 0, 1, columnCount)把整行所有 cell 填进集合。反过来,在更新作用域之外直接向SelectedCells增删单元格会抛InvalidOperationException(DataGrid.cs:4116-4118,“CannotSelectCell”)。 - Cell 下行选择被系统性禁止:
CanSelectRows返回 false(DataGrid.cs:5308);IsSynchronizedWithCurrentItem被 coerce 为 false(DataGrid.cs:1022,因为当前项同步会隐式选中整行);行头点击无效。此时SelectedCells是唯一的选择状态来源。 - CellOrRowHeader 是双向联动模式:行被选中 → 整行 cells 填充;取消某个 cell → 其所在行也从
SelectedItems移除(DataGrid.cs:4182、5114);但选中一行的全部 cells 不会自动选中该行(枚举注释明确说明)。
SelectionUnit 切换:全量清空(Full Wipe)
OnSelectionUnitChanged(DataGrid.cs:4470)做”全量清空”而非模式间转换:
// Full wipe on unit change
if (oldUnit != DataGridSelectionUnit.Cell) { dataGrid.UnselectAll(); }
if (oldUnit != DataGridSelectionUnit.FullRow) { ...; dataGrid._selectedCells.Clear(); }
dataGrid.CoerceValue(IsSynchronizedWithCurrentItemProperty);
切换 SelectionUnit 会丢弃全部现有选择:旧单位支持行就 UnselectAll(),旧单位支持单元格就清空 _selectedCells,不会把行选择翻译成 cells 或反之。
与 SelectionMode 的组合
官方注释明确 “The SelectionMode and the SelectionUnit properties together define the selection behavior”(DataGrid.cs:4393)。OnSelectionModeChanged(DataGrid.cs:4408)切到 Single 时按 unit 决定保留哪个:
Cell:保留任意 1 个 cell(RemoveAllButOne);CellOrRowHeader:保留当前选中行的那一组 cells(RemoveAllButOneRow)。
实践建议
- 订阅
SelectedCellsChanged而非SelectionChanged来感知任何模式下的选择变化。 FullRow下不要代码直改SelectedCells(会抛异常),改用SelectedItems或SelectAll()。Cell下SelectedItems恒为空;需要行语义时自行从SelectedCells按 Item 分组。- 遍历
SelectedCells前检查DataGridCellInfo.IsValid,集合元素可能指向已删除的数据。
图示
行为矩阵、核心类型类图与三种 SelectionUnit 的选择时序: