Curious behaviour of the DataGridViewControl
cost me quite some time today.
TL;DR: Rows.Add(MyOwnFancyRow)
adds a clone of MyOwnFancyRow
to the DGV whereas Rows.AddRange(new MWERow[]{MyOwnFancyRow})
adds MyOwnFancyRow
itself. Figured that out after writing all of it, but still do not understand why.
Hypothesis: DataGridView.Rows.Add()
and DataGridView.Rows.AddRange()
handle the insertion of new rows based on a RowTemplate
differently.
Experiment: I work with unbound DataGridView
s, designed to display complex classes and give the user ways to configure instances of this classes to their taste. I may load some instances from XML and display them, as well as giving the user the opportunity to add new instances (think dgv.AllowUserToAddRows = true;
)
To handle the proper displaying of these instances I create custom DataGridViewRow
s to handle all the interaction between user and instance. This way I can plug my instance into another DataGridView
on a totally different form without having to copy all the handling code. The row does it all for me.
Please consider this not overly complex class for my minimal (non) working example:
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);
}
}
This class on its own cannot comfortably go into a DataGridView
, so here is the custom DataGridViewRow
to handle the displaying and updating (I tried to keep it as complete but brief as possible, so the concept gets clear and I can get pointed to errors):
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();
}
}
}
The concept behind this row is the following:
- A static method allows me to set up any
DataGridView
I want - The
DataGridView
gets assigned specific event handlers to handle things as adding rows and editing cells => this keeps the code of my form clean and I have a way to display the correct values after adding the row - Columns are added to the
DataGridView
- The
CellTemplate
gets set to the most basic row => The one where the object withing gets instantiated with default values (Shows values in the "Add new Row"-Row, defaults are always nice) - Note the Clone method (which has to be there according to the documentation). We will move the comment around a bit in a short while.
The Row itself starts off by creating the cells it requires (CreateCells(templateDGV);
), which is the reason I have to provide a link to the DataGridView this row is going to be added to => I need the column names.
To top everything off we need a form with 1 DataGridView
called dgv and 6 Button
s, so here it is:
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());
}
}
Short description of what the buttons do:
- One button sets the DataGridView up
- One button displays: the "theObject"s from all rows, the content of the "theObjects"-List (fixed) and the content of the "theSecondObjects"-List (growing with each add)
- One button Adds new row instances to the DGV one-by-one (Rows.Add()) "Single Add"
- One button Adds new row instances to the DGV by bulk (Rows.AddRange()) "Bulk Add"
- One button Adds new rows based on the "theObjects"-List one-by-one (Rows.Add()) "Single Add List"
- One button Adds new rows based on the "theObjects"-List by bulk (Rows.AddRange()) "Bulk Add List"
Observations: We will vary the Clone() method step by step. We start with the simpelest clone: return new MWERow(templateDGV);
- Single Add: All entries in the DGV show the default values and their "theObject"s are on Default values
- Bulk Add: All entries in the DGV show the values they are supposed to show and the "theObject"s have the correct values
- Single Add List: Same behaviour as "Single Add"
- Bulk Add List: Same behaviour as "Bulk Add"
- Editing each 2nd entry of each block shows that:
- All Rows are properly connected to the objects they contain (Display 1 (from row) and 3 (List where everything gets added) match
- Only the rows added through "AddRange()" keep the connection to the objects they originate from (this can only be tested for those items in the "theObjects"-List because their instances exist out of the adding scope).
- Adding the objects from the "theObjects"-List twice (by bulk!) shows that changing one element (say the third) changes every row that links to this element => The row is working in principle
Moving on to cloning the half-cooked way: return new MWERow((MWEObject)theObject, templateDGV);
- No matter how the adding is done, all rows show the values they are supposed to show and contain the values they are supposed to contain
- Editing one entry changes all other entries based on the same object, regardless of the exact way how it is added
- When adding a new row by using the "Add new row here"-Row, the values in that row start defaulting to whichever row is last in the already added collection
The last step, going for a full clone: return new MWERow((MWEObject)theObject.Clone(), templateDGV);
- No matter how the adding is done, all rows show what they are supposed to show
- Only those rows that are added by bulk retain a connection to the same object in the background and reflect its changes correctly
- When adding a new row, every new "new row" has the default values as it should have
These observations are consistent, regardless of AllowUserToAddRows = true/false;
and subscribing to dgv.RowsAdded
in the form or not. (There might still be an issue with my index handling of the rowAdded-Event though)
Conclusion:
Short: WTF?
Long: There seems to be something wrong with how new rows are created. The way I understand the documentation, the general procedure is the following:
- An instance of a row is given as
RowTemplate
. I set this template to show the default values - For the "new Row"-Row a clone of this template is created.
- Upon editing this "new Row", it moves into the realm of edited rows, my value moving trickery can take place so that the object inside the row can reflect the newly edited values
- Since this clone of a new row is now a fully functional row, we need a new "new Row", so we clone the template to have a fresh start.
When adding rows programatically the template row should not be of any consequence, since we are providing a fully grown row, complete with a non-default object. The only way I can explain the observed behaviour is this:
- Using
Rows.Add()
takes the row and inserts a clone of it into the DGV - Using
Rows.AddRange()
inserts the row itself
Because I just thought of that so clearly I tested it with a property assigned to the row that gets not copied in the clone method (Or which gets set to "Cloned" inside the clone method). When using the Add()
-Method the property's value is gone, when using AddRange()
it is there. The rest of the behaviour is in line with what I wrote above and makes sense when the whole process of cloning is indeed involved.
Question: Why does Rows.AddRange()
behave differently to Rows.Add()
? What have I missed? Why is there no detailed guide to writing ones own DataGridViewRow
around? Is this by design?
Sorry for this long post but I wanted to make my problem clear and show all I have tested... And I seem to have figured out what is happening, I am just left to wonder about the why...