今天这种奇怪的行为让DataGridViewControl
我花了相当多的时间。
TL;DR :向 DGVRows.Add(MyOwnFancyRow)
添加一个克隆,同时添加自身。写完后想通了,但还是不明白为什么。MyOwnFancyRow
Rows.AddRange(new MWERow[]{MyOwnFancyRow})
MyOwnFancyRow
假设: DataGridView.Rows.Add()
并DataGridView.Rows.AddRange()
根据RowTemplate
不同的方式处理新行的插入。
实验:我使用unbound DataGridView
s,旨在显示复杂的类,并为用户提供配置此类实例的方法以满足他们的口味。我可能会从 XML 加载一些实例并显示它们,并让用户有机会添加新实例(想想dgv.AllowUserToAddRows = true;
)
为了处理这些实例的正确显示,我创建了 customDataGridViewRow
来处理用户和实例之间的所有交互。通过这种方式,我可以将我的实例DataGridView
以完全不同的形式插入另一个实例,而无需复制所有处理代码。该行为我完成了这一切。
对于我的最小(非)工作示例,请考虑这个不太复杂的类:
class MWEObject : ICloneable {
public string Value1 { get; internal set; }
public double Value2 { get; internal set; }
public delegate void OnMWEObjectChanged();
public event OnMWEObjectChanged ObjectChanged;
public MWEObject() {
Value1 = "Default"; Value2 = 0;
}
public MWEObject(string in_Value1, double in_Value2) {
Value1 = in_Value1; Value2 = in_Value2;
}
public void UpdateObject(MWEObject in_Object) {
Value1 = in_Object.Value1;
Value2 = in_Object.Value2;
if (ObjectChanged != null)
ObjectChanged.Invoke();
}
public override string ToString() {
return "Value1: " + Value1 + " Value2: " + Value2.ToString("0.000");
}
public object Clone() {
return new MWEObject(Value1, Value2);
}
}
这个类本身不能舒适地进入 a DataGridView
,所以这里是DataGridViewRow
处理显示和更新的自定义(我试图让它尽可能完整但简短,所以概念变得清晰,我可以指出错误):
class MWERow : DataGridViewRow {
public MWEObject theObject { get; private set; }
DataGridView templateDGV;
bool changing;
public MWERow(DataGridView in_template) {
templateDGV = in_template;
theObject = new MWEObject();
changing = false;
CreateCells(templateDGV);
}
public MWERow(MWEObject in_Object, DataGridView in_template): this(in_template) {
theObject = in_Object;
theObject.ObjectChanged += TheObject_ObjectChanged;
}
private void TheObject_ObjectChanged() {
if (!changing)
SetupValues();
}
private void RowAdded() {
SetupValues();
}
private void CellChanged(DataGridViewCellEventArgs e) {
if (DataGridView != null && !changing)
theObject.UpdateObject(
new MWEObject(
e.ColumnIndex == Cells["Value1"].ColumnIndex ? (string)Cells["Value1"].Value : theObject.Value1,
e.ColumnIndex == Cells["Value2"].ColumnIndex ? (double)Cells["Value2"].Value : theObject.Value2));
}
private void SetupValues() {
if (DataGridView != null && Cells["Value1"].RowIndex >= 0)
{
changing = true;
Cells["Value1"].Value = theObject.Value1;
Cells["Value2"].Value = theObject.Value2;
changing = false;
}
}
public override object Clone() {
return new MWERow(/*(MWEObject)theObject.Clone(),*/templateDGV);
}
// Clever telling the DataGridView what to do starts here
public static void SetupDGV(DataGridView theDGV) {
bool oldState = theDGV.AllowUserToAddRows;
theDGV.AllowUserToAddRows = false;
theDGV.Rows.Clear();
theDGV.Columns.Clear();
theDGV.RowsAdded -= TheDGV_RowsAdded;
theDGV.RowsAdded += TheDGV_RowsAdded;
theDGV.CellEndEdit -= TheDGV_CellEndEdit;
theDGV.CellEndEdit += TheDGV_CellEndEdit;
theDGV.CurrentCellDirtyStateChanged -= TheDGV_CurrentCellDirtyStateChanged;
theDGV.CurrentCellDirtyStateChanged += TheDGV_CurrentCellDirtyStateChanged;
theDGV.Columns.Add(new DataGridViewTextBoxColumn() { Name = "Value1", HeaderText = "Value 1", CellTemplate = new DataGridViewTextBoxCell() { ValueType = typeof(string) } });
theDGV.Columns.Add(new DataGridViewTextBoxColumn() { Name = "Value2", HeaderText = "Value 2", CellTemplate = new DataGridViewTextBoxCell() { ValueType = typeof(double) } });
theDGV.RowTemplate = new MWERow(theDGV);
theDGV.AllowUserToAddRows = oldState;
}
private static void TheDGV_CurrentCellDirtyStateChanged(object sender, EventArgs e) {
if (((DataGridView)sender).IsCurrentCellDirty)
((DataGridView)sender).CommitEdit(DataGridViewDataErrorContexts.Commit);
}
private static void TheDGV_CellEndEdit(object sender, DataGridViewCellEventArgs e) {
if (((DataGridView)sender).Columns[e.ColumnIndex].GetType() == typeof(DataGridViewTextBoxColumn))
((MWERow)(((DataGridView)sender).Rows[e.RowIndex])).CellChanged(e);
}
private static void TheDGV_RowsAdded(object sender, DataGridViewRowsAddedEventArgs e) {
DataGridView dgv = sender as DataGridView;
if (dgv != null)
{
int StartIndex = e.RowIndex;
if ((dgv.AllowUserToAddRows && StartIndex != 0) && StartIndex == dgv.Rows.Count - 1)
StartIndex--;
foreach (int i in Enumerable.Range(StartIndex, e.RowCount))
((MWERow)dgv.Rows[i]).RowAdded();
if (dgv.AllowUserToAddRows && e.RowIndex == dgv.Rows.Count - 1)
((MWERow)dgv.Rows[dgv.Rows.Count - 1]).RowAdded();
}
}
}
这一行背后的概念如下:
- 静态方法允许我设置任何
DataGridView
我想要的 DataGridView
分配了特定的事件处理程序来处理添加行和编辑单元格 => 这使我的表单代码保持干净,并且我有办法在添加行后显示正确的值- 列被添加到
DataGridView
- 设置为最基本的
CellTemplate
行 => 使用默认值实例化对象的行(在“添加新行”-行中显示值,默认值总是很好) - 请注意克隆方法(根据文档必须存在)。我们将在短时间内移动评论。
Row 本身首先创建它需要的单元格 ( CreateCells(templateDGV);
),这就是我必须提供指向 DataGridView 的链接的原因,该行将被添加到 => 我需要列名。
最重要的是,我们需要一个DataGridView
名为 dgv 和 6 Button
s 的表格,所以这里是:
public partial class frm_Test : Form {
List<MWEObject> theObjects;
List<MWEObject> theSecondObjects;
public frm_Test() {
InitializeComponent();
theObjects = new List<MWEObject>()
{
new MWEObject("test 1", 1),
new MWEObject("test 2", 2),
new MWEObject("test 3", 3),
new MWEObject("test 4", 4)
};
theSecondObjects = new List<MWEObject>();
}
private void btn_SetupDGV_Click(object sender, EventArgs e) {
MWERow.SetupDGV(dgv);
dgv.RowsAdded += Dgv_RowsAdded;
}
private void Dgv_RowsAdded(object sender, DataGridViewRowsAddedEventArgs e) {
DataGridView dgv = sender as DataGridView;
if (dgv != null)
{
int StartIndex = e.RowIndex;
if ((dgv.AllowUserToAddRows && StartIndex != 0) && StartIndex == dgv.Rows.Count - 1)
StartIndex--;
foreach (int i in Enumerable.Range(StartIndex, e.RowCount))
theSecondObjects.Add(((MWERow)dgv.Rows[i]).theObject);
}
}
private void btn_ShowLists_Click(object sender, EventArgs e) {
MessageBox.Show("Rows: " + Environment.NewLine + string.Join(Environment.NewLine, dgv.Rows.Cast<MWERow>().Select(x => x.theObject.ToString() + (x.IsNewRow ? " isNew" : ""))));
MessageBox.Show("List: " + Environment.NewLine + string.Join(Environment.NewLine, theObjects.Select(x => x.ToString())));
MessageBox.Show("Second List: " + Environment.NewLine + string.Join(Environment.NewLine, theSecondObjects.Select(x => x.ToString())));
}
private void btn_AddSingleItem_Click(object sender, EventArgs e) {
dgv.Rows.Add(new MWERow(new MWEObject("1 Test", 1), dgv));
dgv.Rows.Add(new MWERow(new MWEObject("2 Test", 2), dgv));
dgv.Rows.Add(new MWERow(new MWEObject("3 Test", 3), dgv));
dgv.Rows.Add(new MWERow(new MWEObject("4 Test", 4), dgv));
dgv.Rows.Add(new MWERow(new MWEObject("5 Test", 5), dgv));
}
private void btn_AddBulkItems_Click(object sender, EventArgs e) {
dgv.Rows.AddRange(new MWERow[]
{
new MWERow(new MWEObject("1 Test", 1), dgv),
new MWERow(new MWEObject("2 Test", 2), dgv),
new MWERow(new MWEObject("3 Test", 3), dgv),
new MWERow(new MWEObject("4 Test", 4), dgv),
new MWERow(new MWEObject("5 Test", 5), dgv)
});
}
private void btn_AddSingleList_Click(object sender, EventArgs e) {
foreach (MWEObject o in theObjects)
dgv.Rows.Add(new MWERow(o, dgv));
}
private void btn_AddBulkList_Click(object sender, EventArgs e) {
dgv.Rows.AddRange(theObjects.Select(x => new MWERow(x, dgv)).ToArray());
}
}
按钮功能的简短描述:
- 一键设置 DataGridView
- 一键显示:所有行中的“theObject”、“theObjects”-List 的内容(固定)和“theSecondObjects”-List 的内容(随着每次添加而增长)
- 一键将新行实例一一添加到 DGV (Rows.Add()) “单添加”
- 一键将新行实例批量添加到 DGV (Rows.AddRange()) “批量添加”
- 一键根据“theObjects”添加新行 - 逐一列表(Rows.Add())“单添加列表”
- 一键根据“theObjects”添加新行-批量列表(Rows.AddRange())“批量添加列表”
观察:我们将逐步改变 Clone() 方法。我们从最简单的克隆开始:return new MWERow(templateDGV);
- 单次添加:DGV 中的所有条目都显示默认值,并且它们的“theObject”位于默认值上
- 批量添加:DGV 中的所有条目都显示它们应该显示的值,并且“theObject”具有正确的值
- 单一添加列表:与“单一添加”相同的行为
- 批量添加列表:与“批量添加”相同的行为
- 编辑每个块的每个第二个条目表明:
- 所有行都正确连接到它们包含的对象(显示 1(从行)和 3(添加所有内容的列表)匹配
- 只有通过“AddRange()”添加的行保持与它们来源的对象的连接(这只能针对“theObjects”列表中的那些项目进行测试,因为它们的实例存在于添加范围之外)。
- 从“theObjects”列表中添加对象两次(批量!)表明更改一个元素(比如第三个)会更改链接到该元素的每一行=>该行原则上工作
继续克隆半熟的方式:return new MWERow((MWEObject)theObject, templateDGV);
- 无论添加如何完成,所有行都显示它们应该显示的值并包含它们应该包含的值
- 编辑一个条目会更改基于同一对象的所有其他条目,无论添加的确切方式如何
- 使用“在此处添加新行”-Row 添加新行时,该行中的值开始默认为已添加集合中的最后一行
最后一步,进行完整克隆:return new MWERow((MWEObject)theObject.Clone(), templateDGV);
- 无论添加如何完成,所有行都显示它们应该显示的内容
- 只有批量添加的那些行在后台保留与同一对象的连接并正确反映其更改
- 添加新行时,每个新的“新行”都具有应具有的默认值
这些观察结果是一致的,无论是否AllowUserToAddRows = true/false;
订阅dgv.RowsAdded
表格。(虽然我对 rowAdded-Event 的索引处理可能仍然存在问题)
结论:
短:WTF?
Long:新行的创建方式似乎有问题。我理解文档的方式,一般程序如下:
- 行的一个实例给出为
RowTemplate
。我将此模板设置为显示默认值 - 对于“新行”-行,创建此模板的克隆。
- 编辑此“新行”后,它会移动到已编辑行的领域,我的值移动技巧可以发生,以便行内的对象可以反映新编辑的值
- 由于这个新行的克隆现在是一个功能齐全的行,我们需要一个新的“新行”,所以我们克隆模板以重新开始。
当以编程方式添加行时,模板行不应该有任何后果,因为我们提供了一个完全增长的行,并带有一个非默认对象。我可以解释观察到的行为的唯一方法是:
- 使用
Rows.Add()
获取该行并将其克隆插入 DGV - 使用
Rows.AddRange()
插入行本身
因为我只是清楚地想到了这一点,所以我使用分配给未在克隆方法中复制的行的属性(或者在克隆方法中设置为“克隆”)对其进行了测试。使用Add()
-Method 时,该属性的值消失了,使用时AddRange()
它就在那里。其余的行为与我上面写的一致,并且当确实涉及克隆的整个过程时才有意义。
问题:为什么Rows.AddRange()
表现不一样Rows.Add()
?我错过了什么?为什么没有详细的编写自己的指南DataGridViewRow
?这是设计使然吗?
很抱歉这篇长篇文章,但我想澄清我的问题并展示我测试过的所有内容......而且我似乎已经弄清楚发生了什么,我只是想知道为什么......