DataGridView插入/删除/排序行时自定义单元格被销毁如何解决
问题根因
你遇到的单元格重置问题是DataGridView的默认机制导致的:当控件执行排序、新增/删除行、重置绑定这类操作时,会自动按照每列配置的默认CellTemplate(也就是你第5列默认的DataGridViewTextBoxCell模板)重新生成所有行的对应单元格。你在初始化阶段手动遍历替换单元格的逻辑仅执行一次,后续行重建时自定义的下拉单元格就会被默认模板生成的文本框单元格覆盖。
你提到的「在单元格生成节点介入修改类型」的思路是正确的,不需要找不存在的BeforeCellCreation事件,有两种成熟的实现方式可以解决这个问题。另外你提供的示例代码存在语法错误:myGrid(customCol, r) = cell是VB.NET的索引器写法,C#环境下需要写成myGrid[customCol, r] = cell才能正常编译。
实现方案
方案1:绑定完成事件重替换(改动最小,适配现有代码)
把你原来遍历替换单元格的逻辑,迁移到DataGridView的DataBindingComplete事件中即可。这个事件会在绑定初始化、排序、增删行、绑定重置所有会触发单元格重建的操作完成后触发,能保证每次单元格被重置后都重新替换为目标类型。
为了避免性能损耗和值丢失,需要加类型判断跳过已替换的单元格,同时在替换前缓存原单元格的值:
// 初始化绑定 myGrid.DataSource = bs; bs.DataSource = myDataSource; // 注册绑定完成事件,所有触发单元格重建的操作完成后都会执行该逻辑 myGrid.DataBindingComplete += (s, e) => { const int customColIndex = 5; // 遍历所有数据行,跳过用户编辑用的新增空行 foreach (DataGridViewRow row in myGrid.Rows.Cast<DataGridViewRow>().Where(r => !r.IsNewRow)) { if (row.DataBoundItem is MyType dataRow && dataRow.SomeVal) { // 当前单元格已经是下拉框类型则跳过,避免重复创建 if (row.Cells[customColIndex] is DataGridViewComboBoxCell) continue; // 缓存原单元格的值,替换后还原避免数据丢失 var originalValue = row.Cells[customColIndex].Value; var comboCell = new DataGridViewComboBoxCell { DataSource = MyList, ValueMember = "ID", DisplayMember = "Desc" }; row.Cells[customColIndex] = comboCell; comboCell.Value = originalValue; } else { // 行状态变更为不需要下拉框时,切回默认文本框单元格 if (row.Cells[customColIndex] is not DataGridViewTextBoxCell) { var originalValue = row.Cells[customColIndex].Value; row.Cells[customColIndex] = new DataGridViewTextBoxCell(); row.Cells[customColIndex].Value = originalValue; } } } }; bs.ResetBindings(true);
方案2:自定义列重写单元格创建逻辑(适合复用场景)
如果这套动态单元格逻辑需要在多个网格/列中复用,可以自定义专用列,重写单元格生成逻辑,不需要每次事件触发都遍历全量行替换:
public class DynamicTypeColumn : DataGridViewTextBoxColumn { // 传入判断逻辑:入参为当前行绑定的数据对象,返回true则用下拉单元格,否则用默认文本单元格 public Func<object, bool> UseComboBoxJudge { get; set; } public object ComboBoxDataSource { get; set; } public string ComboBoxValueMember { get; set; } public string ComboBoxDisplayMember { get; set; } protected override DataGridViewCell GetCell(int rowIndex) { var boundItem = DataGridView?.Rows[rowIndex].DataBoundItem; if (boundItem != null && UseComboBoxJudge != null && UseComboBoxJudge(boundItem)) { return new DataGridViewComboBoxCell { DataSource = ComboBoxDataSource, ValueMember = ComboBoxValueMember, DisplayMember = ComboBoxDisplayMember }; } return base.GetCell(rowIndex); } }
使用时直接替换原有第5列即可,不需要额外写遍历逻辑:
// 移除原来的默认文本列 myGrid.Columns.RemoveAt(5); // 插入自定义动态列 var dynamicCol = new DynamicTypeColumn { DataPropertyName = "原列绑定的字段名", // 和原列绑定的实体字段名保持一致 HeaderText = "原列标题", UseComboBoxJudge = rowObj => rowObj is MyType t && t.SomeVal, ComboBoxDataSource = MyList, ComboBoxValueMember = "ID", ComboBoxDisplayMember = "Desc" }; myGrid.Columns.Insert(5, dynamicCol);
注意事项
- 替换单元格前必须缓存原单元格的
Value属性,替换完成后重新赋值,否则会出现值丢失、单元格显示空白的问题 - 遍历行时必须跳过
IsNewRow=true的用户新增空行,避免在未输入的新行上生成错误类型的单元格 - 不要在
CellPainting、RowPrePaint这类高频触发的渲染事件中执行单元格替换逻辑,会引发性能损耗甚至渲染死循环
内容的提问来源于stack exchange,提问作者Disti
相关产品推荐
相关产品推荐

