Data errors with a data-bound DataGridViewComboBoxColumn

EDN Admin

Well-known member
Joined
Aug 7, 2010
Messages
12,794
Location
In the Machine
I’m trying to resolve an issue I’m experiencing with a DataGridView bound to a data object that includes a DataGridViewComboBoxColumn that is bound to a list on the data object to provide values for the drop down. The user should be able to add values to
the DataGridViewComboBoxColumn.
However, investigating in the debugger one finds the DataGridViewComboBoxCells Items list does not contain the item that was added to the data object (model) during edit of another cell, however, the cells DataSource List contains the correct values. In
addition, the DataGridViewComboBoxColumns Items collection contains the correct values. Why are the data-bound cells Items not being updated when the data object is updated?
I would appreciate any guidance on how to correctly perform this type of work.
The dataGridView1_CellValidating method has blocks of commented out code showing the various techniques I’ve tried while working to resolve this issue. The hack of setting the DataGridViewComboBoxCell’s DataSource to null and then back to the binding source
appeared to work, but upon further testing I discovered the issue still persists.
One can recreate the issue with the following steps:
<ol>
<span style="font-family:Times New Roman; font-size:7pt; line-height:normal
Type "one" into the LeftElementName combobox of the first row<span style="font-family:Times New Roman; font-size:7pt; line-height:normal
Click into the LeftElementName combobox of the second row<span style="font-family:Times New Roman; font-size:7pt; line-height:normal
Click the dropdown button and select the value "one"<span style="font-family:Times New Roman; font-size:7pt; line-height:normal
Click into the LeftElementName combobox of the third row<span style="font-family:Times New Roman; font-size:7pt; line-height:normal
Click into the LeftElementName combobox of the second row<span style="font-family:Times New Roman; font-size:7pt; line-height:normal
Type "two" into the LeftElementName combobox of the second row<span style="font-family:Times New Roman; font-size:7pt; line-height:normal
Click into the LeftElementName combobox of the third row
<ol>
<span style="font-family:Times New Roman; font-size:7pt; line-height:normal
The DataGridView fires a DataError event for the LeftElementName column of the second row - "DataGridViewComboBoxCell value is not valid".</ol>
<span style="font-family:Times New Roman; font-size:7pt; line-height:normal
Click the dropdown button and select the value "two"<span style="font-family:Times New Roman; font-size:7pt; line-height:normal
Click into the RightElementName combobox of this row (the third row)
<ol>
<span style="font-family:Times New Roman; font-size:7pt; line-height:normal
The DataGridView fires a total of 4 DataError events for the LeftElementName column of the third row - "DataGridViewComboBoxCell value is not valid".</ol>
</ol>
<span style="font-size:11.0pt; line-height:115%; font-family:Calibri,sans-serif
MappingDialog.cs:<span style="font-size:11.0pt; line-height:115%; font-family:Calibri,sans-serif
<pre class="prettyprint using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace ComboBoxBindingIssue
{
public partial class MappingDialog : Form
{
private int _leftElementColIdx;
private int _rightElementColIdx;
private BindingSource _leftElementsBinding;
private BindingSource _rightElementsBinding;

#region Properties

/// <summary>
/// Gets or sets the model.
/// </summary>
public Mappings Model { get; set; }

#endregion Properties

/// <summary>
/// Initializes a new instance of the MappingDialog class.
/// </summary>
public MappingDialog()
{
_leftElementColIdx = -1;
_rightElementColIdx = -1;
InitializeComponent();

this.dataGridView1.CellValidating += new System.Windows.Forms.DataGridViewCellValidatingEventHandler(this.dataGridView1_CellValidating);
this.dataGridView1.DataError += new System.Windows.Forms.DataGridViewDataErrorEventHandler(this.dataGridView1_DataError);
this.dataGridView1.EditingControlShowing += new System.Windows.Forms.DataGridViewEditingControlShowingEventHandler(this.dataGridView1_EditingControlShowing);
}

private void Form1_Load(object sender, EventArgs e)
{
BindingSource binding1 = new BindingSource();
binding1.DataSource = Model;
binding1.DataMember = "Collection";

// initialize the DataGridView
this.dataGridView1.AutoGenerateColumns = false;
this.dataGridView1.AutoSize = true;
this.dataGridView1.DataSource = binding1;

// initialize and add a column for the LeftElementName
DataGridViewColumn leftElementCol;
if (Model.LeftElements.Count > 0)
{
// initialize and add a combo box column for LeftElementName
leftElementCol = new DataGridViewComboBoxColumn();
((DataGridViewComboBoxColumn)leftElementCol).DisplayStyle = DataGridViewComboBoxDisplayStyle.ComboBox;
((DataGridViewComboBoxColumn)leftElementCol).FlatStyle = FlatStyle.Flat;

_leftElementsBinding = new BindingSource();
_leftElementsBinding.DataSource = Model;
_leftElementsBinding.DataMember = "LeftElements";
((DataGridViewComboBoxColumn)leftElementCol).DataSource = _leftElementsBinding;
}
else
{
// initialize and add a text box column for LeftElementName
leftElementCol = new DataGridViewTextBoxColumn();
leftElementCol.ReadOnly = true;
}
leftElementCol.DataPropertyName = "LeftElementName";
leftElementCol.Name = "LeftElementName";
_leftElementColIdx = this.dataGridView1.Columns.Add(leftElementCol);

// initialize and add a column for the RightElementName
DataGridViewColumn rightElementCol;
if (Model.RightElements.Count > 0)
{
// initialize and add a combo box column for RightElementName
rightElementCol = new DataGridViewComboBoxColumn();
((DataGridViewComboBoxColumn)rightElementCol).DisplayStyle = DataGridViewComboBoxDisplayStyle.ComboBox;
((DataGridViewComboBoxColumn)rightElementCol).FlatStyle = FlatStyle.Flat;

_rightElementsBinding = new BindingSource();
_rightElementsBinding.DataSource = Model;
_rightElementsBinding.DataMember = "RightElements";
((DataGridViewComboBoxColumn)rightElementCol).DataSource = _rightElementsBinding;
}
else
{
// initialize and add a text box column for RightElementName
rightElementCol = new DataGridViewTextBoxColumn();
rightElementCol.ReadOnly = true;
}
rightElementCol.DataPropertyName = "RightElementName";
rightElementCol.Name = "RightElementName";
_rightElementColIdx = this.dataGridView1.Columns.Add(rightElementCol);

// initialize and add a combo box column for Direction
DataGridViewComboBoxColumn comboColumn = new DataGridViewComboBoxColumn();
comboColumn.DataSource = Enum.GetValues(typeof(MappingDirection));
comboColumn.DataPropertyName = "Direction";
comboColumn.Name = "Direction";
this.dataGridView1.Columns.Add(comboColumn);

// resize the DataGridView columns to fit the newly loaded data
this.dataGridView1.AutoResizeColumns();
}

private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
DataGridViewComboBoxEditingControl comboBox = e.Control as DataGridViewComboBoxEditingControl;
if (comboBox != null
&& (comboBox.EditingControlDataGridView.CurrentCell.ColumnIndex == _leftElementColIdx
|| comboBox.EditingControlDataGridView.CurrentCell.ColumnIndex == _rightElementColIdx))
{
// remove an existing event-handler, if present, to avoid
// adding multiple handlers when the editing control is reused
//comboBox.Validating -= new CancelEventHandler(comboBox_Validating);

comboBox.DropDownStyle = ComboBoxStyle.DropDown;

// add event-handler
//comboBox.Validating += new CancelEventHandler(comboBox_Validating);
}
}

private void comboBox_Validating(object sender, CancelEventArgs e)
{
// Attempt to get the values stored at a different point - didnt seem to resolve the issue
DataGridViewComboBoxEditingControl comboBox = sender as DataGridViewComboBoxEditingControl;
if (comboBox != null)
{
DataGridView dgv = comboBox.EditingControlDataGridView;
// add value if it does not exist
if (!String.IsNullOrWhiteSpace(comboBox.Text)
&& !Model.LeftElements.Contains<String>(comboBox.Text))
{
DataGridViewComboBoxColumn comboColumn = dgv.Columns[dgv.CurrentCell.ColumnIndex] as DataGridViewComboBoxColumn;

Model.LeftElements.Add(comboBox.Text);
comboColumn.DataSource = null;
comboColumn.DataSource = _leftElementsBinding;
dgv.CurrentCell.Value = comboBox.Text;
System.Diagnostics.Debug.WriteLine("t Added Value to LeftElements");
}
}
}

private void dataGridView1_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
System.Diagnostics.Debug.WriteLine(String.Format("dataGridView1_CellValidating: Row: {0}, Col: {1}, FormattedValue: {2}", e.RowIndex, e.ColumnIndex, e.FormattedValue));
DataGridViewComboBoxCell dgvComboBoxCell = ((DataGridView)sender)[e.ColumnIndex, e.RowIndex] as DataGridViewComboBoxCell;
if (e.ColumnIndex == _leftElementColIdx
&& dgvComboBoxCell != null
&& !String.IsNullOrWhiteSpace(e.FormattedValue.ToString())
&& !Model.LeftElements.Contains<String>(e.FormattedValue.ToString()))
{
// add new element item to model
Model.LeftElements.Add(e.FormattedValue.ToString());

// Attempt #1 - Following from advice in the DataBinding and DataGridView FAQs
// reset the BindingSource data source in order to reset bindings
// Note: the BindingSource provuides a level of indirection at both design-time and run-time
// that alleviates the need for users to reset their bindings or merge data sets.
//((DataGridViewComboBoxCell)((DataGridView)sender)[e.ColumnIndex, e.RowIndex]).DataSource = _leftElementsBinding;
//((DataGridView)sender).CurrentCell.Value = e.FormattedValue;

// Attempt #2 - Method descriptions seemed to describe exactly what I needed -
// "Causes a control bound to the BindingSource to reread all the items in the list and refresh their displayed values."
//_leftElementsBinding.ResetBindings(false);
//_leftElementsBinding.ResetCurrentItem();
//_leftElementsBinding.ResetItem(_mappings.LeftElements.Count - 1);

// Attempt #3 - Thrashing around at this point – came across a forum post that mentioned this working for someone
//((DataGridView)sender).CommitEdit(DataGridViewDataErrorContexts.Commit);

// Attempt #4 - DataBinding FAQ states this is incorrect, however, it is the only thing I could find to get the DataGridViewComboBoxCells
// Items collection to match the correct list of values most of the time. The issue persists.
// Note: resetting the binding in this manner does not seem correct, however, the cells items collection is updated correctly when performed
//((DataGridViewComboBoxCell)((DataGridView)sender)[e.ColumnIndex, e.RowIndex]).DataSource = null;
//((DataGridViewComboBoxCell)((DataGridView)sender)[e.ColumnIndex, e.RowIndex]).DataSource = _leftElementsBinding;
dgvComboBoxCell.DataSource = null;
dgvComboBoxCell.DataSource = _leftElementsBinding;
((DataGridView)sender).CurrentCell.Value = e.FormattedValue;

System.Diagnostics.Debug.WriteLine("t Added Value to LeftElements");
}
else if (e.ColumnIndex == _rightElementColIdx
&& dgvComboBoxCell != null
&& !String.IsNullOrWhiteSpace(e.FormattedValue.ToString())
&& !Model.RightElements.Contains<String>(e.FormattedValue.ToString()))
{
// add new element item to model
Model.RightElements.Add(e.FormattedValue.ToString());
// Note: resetting the binding in this manner does not seem correct, however, the cells items collection is updated correctly when performed
dgvComboBoxCell.DataSource = null;
dgvComboBoxCell.DataSource = _rightElementsBinding;
((DataGridView)sender).CurrentCell.Value = e.FormattedValue;

System.Diagnostics.Debug.WriteLine("t Added Value to RightElements");
}
}

private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
/*MessageBox.Show(String.Format("({0},{1}): {2}", e.RowIndex, e.ColumnIndex, e.Exception.Message),
"Data Error",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);*/

// the DataGridViewComboBoxCells Items list does not contain the item that was added to the model during edit of another cell, however,
// this cells DataSource List contains the correct values. Why isnt the cells Items being updated?

System.Diagnostics.Debug.WriteLine(String.Format("({0},{1}): {2}", e.RowIndex, e.ColumnIndex, e.Exception.Message));
}
}
}
[/code]
MappingDialog.Designer.cs:
<pre class="prettyprint namespace ComboBoxBindingIssue
{
partial class MappingDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;

/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

#region Windows Form Designer generated code

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.dataGridView1 = new System.Windows.Forms.DataGridView();
this.buttonPanel = new System.Windows.Forms.Panel();
this.cancelButton = new System.Windows.Forms.Button();
this.okButton = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
this.buttonPanel.SuspendLayout();
this.SuspendLayout();
//
// dataGridView1
//
this.dataGridView1.AllowUserToAddRows = false;
this.dataGridView1.AllowUserToDeleteRows = false;
this.dataGridView1.AllowUserToResizeRows = false;
this.dataGridView1.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells;
this.dataGridView1.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridView1.Dock = System.Windows.Forms.DockStyle.Fill;
this.dataGridView1.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnKeystroke;
this.dataGridView1.Location = new System.Drawing.Point(0, 0);
this.dataGridView1.Name = "dataGridView1";
this.dataGridView1.RowHeadersVisible = false;
this.dataGridView1.ShowCellErrors = false;
this.dataGridView1.ShowEditingIcon = false;
this.dataGridView1.ShowRowErrors = false;
this.dataGridView1.Size = new System.Drawing.Size(283, 232);
this.dataGridView1.TabIndex = 0;
this.dataGridView1.CellValidating += new System.Windows.Forms.DataGridViewCellValidatingEventHandler(this.dataGridView1_CellValidating);
this.dataGridView1.DataError += new System.Windows.Forms.DataGridViewDataErrorEventHandler(this.dataGridView1_DataError);
this.dataGridView1.EditingControlShowing += new System.Windows.Forms.DataGridViewEditingControlShowingEventHandler(this.dataGridView1_EditingControlShowing);
//
// buttonPanel
//
this.buttonPanel.Controls.Add(this.cancelButton);
this.buttonPanel.Controls.Add(this.okButton);
this.buttonPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
this.buttonPanel.Location = new System.Drawing.Point(0, 203);
this.buttonPanel.Name = "buttonPanel";
this.buttonPanel.Size = new System.Drawing.Size(283, 29);
this.buttonPanel.TabIndex = 1;
//
// cancelButton
//
this.cancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelButton.Location = new System.Drawing.Point(196, 3);
this.cancelButton.Name = "cancelButton";
this.cancelButton.Size = new System.Drawing.Size(75, 23);
this.cancelButton.TabIndex = 1;
this.cancelButton.Text = "Cancel";
this.cancelButton.UseVisualStyleBackColor = true;
//
// okButton
//
this.okButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.okButton.DialogResult = System.Windows.Forms.DialogResult.OK;
this.okButton.Location = new System.Drawing.Point(115, 3);
this.okButton.Name = "okButton";
this.okButton.Size = new System.Drawing.Size(75, 23);
this.okButton.TabIndex = 0;
this.okButton.Text = "OK";
this.okButton.UseVisualStyleBackColor = true;
//
// MappingDialog
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.AutoSize = true;
this.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.ClientSize = new System.Drawing.Size(283, 232);
this.Controls.Add(this.buttonPanel);
this.Controls.Add(this.dataGridView1);
this.MinimizeBox = false;
this.Name = "MappingDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.Text = "Mapping";
this.Load += new System.EventHandler(this.Form1_Load);
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
this.buttonPanel.ResumeLayout(false);
this.ResumeLayout(false);

}

#endregion

private System.Windows.Forms.DataGridView dataGridView1;
private System.Windows.Forms.Panel buttonPanel;
private System.Windows.Forms.Button okButton;
private System.Windows.Forms.Button cancelButton;
}
}[/code]
IMapping.cs:<br/>

<pre class="prettyprint using System;
using System.ComponentModel;

namespace ComboBoxBindingIssue
{
/// <summary>
/// Interface for mapping classes.
/// </summary>
public interface IMapping : INotifyPropertyChanged
{
/// <summary>
/// Gets or sets the name of the left element.
/// </summary>
string LeftElementName { get; set; }

/// <summary>
/// Gets or sets the name of the right element.
/// </summary>
string RightElementName { get; set; }

/// <summary>
/// Gets or sets the mapping direction.
/// </summary>
MappingDirection Direction { get; set; }
}
}[/code]
<br/>

Mapping.cs:
<pre class="prettyprint using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;

namespace ComboBoxBindingIssue
{
/// <summary>
/// Defines a mapping between a left element and right element.
/// </summary>
public class Mapping : IMapping
{
private string _leftElementName;
private string _rightElementName;
private MappingDirection _direction;

#region Properties

/// <summary>
/// Gets or sets the name of the left element.
/// </summary>
public string LeftElementName
{
get
{
return _leftElementName;
}
set
{
if (_leftElementName != value)
{
_leftElementName = value;
OnPropertyChanged("LeftElementName");
}
}
}

/// <summary>
/// Gets or sets the name of the right element.
/// </summary>
public string RightElementName
{
get
{
return _rightElementName;
}
set
{
if (_rightElementName != value)
{
_rightElementName = value;
OnPropertyChanged("RightElementName");
}
}
}

/// <summary>
/// Gets or sets the mapping direction.
/// </summary>
public MappingDirection Direction
{
get
{
return _direction;
}
set
{
if (_direction != value)
{
_direction = value;
OnPropertyChanged("Direction");
}
}
}

#endregion Properties

/// <summary>
/// Event raised when a property is changed on the component.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;

/// <summary>
/// Initializes a new instance of a Mapping class.
/// </summary>
/// <remarks>
/// The left and right elements are set to String.Empty and
/// the mapping direction is set to MappingDirection.None.
/// </remarks>
public Mapping()
: this(String.Empty, String.Empty, MappingDirection.None)
{
}

/// <summary>
/// Initializes a new instance of a Mapping class.
/// </summary>
/// <remarks>
/// The mapping direction is set to MappingDirection.None.
/// </remarks>
/// <param name="leftElementName The name of the left element.</param>
/// <param name="rightElementName The name of the right element.</param>
public Mapping(string leftElementName, string rightElementName)
: this(leftElementName, rightElementName, MappingDirection.None)
{
}

/// <summary>
///Initializes a new instance of a Mapping class.
/// </summary>
/// <remarks>
/// IsValidLeftElementName and IsValidRightElementName are set to true.
/// </remarks>
/// <param name="leftElementName The name of the left element.</param>
/// <param name="rightElementName The name of the right element.</param>
/// <param name="direction The mapping direction.</param>
public Mapping(string leftElementName, string rightElementName, MappingDirection direction)
{
_leftElementName = leftElementName;
_rightElementName = rightElementName;
_direction = direction;
}

/// <summary>
/// Fires a PropertyChanged event to notify clients that all properties changed.
/// </summary>
public void NotifyAllPropertiesChanged()
{
OnPropertyChanged(String.Empty);
}

/// <summary>
/// Fires a PropertyChanged event to notify clients that a property value has changed.
/// </summary>
/// <param name="propertyName The name of the property that changed.</param>
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}[/code]
<br/>

MappingDirection.cs:
<pre class="prettyprint using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ComboBoxBindingIssue
{
/// <summary>
/// The mapping direction.
/// </summary>
public enum MappingDirection
{
None,
Input,
Output
}
}[/code]
<br/>

Mappings.cs:
<pre class="prettyprint using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;

namespace ComboBoxBindingIssue
{
/// <summary>
/// Represents a collection of mappings, as well as left and right element collections.
/// </summary>
public class Mappings
{
#region Properties

/// <summary>
/// The mapping collection.
/// </summary>
public BindingList<Mapping> Collection { get; private set; }

/// <summary>
/// The left element collection.
/// </summary>
/// <remarks>The collection of left elements is optional.</remarks>
public BindingList<String> LeftElements { get; private set; }

/// <summary>
/// The right element collection.
/// </summary>
/// <remarks>The collection of right elements is optional.</remarks>
public BindingList<String> RightElements { get; private set; }

#endregion Properties

/// <summary>
/// Initializes a new instance of a Mappings class.
/// </summary>
public Mappings()
{
Collection = new BindingList<Mapping>();
LeftElements = new BindingList<String>();
RightElements = new BindingList<String>();
}
}
}[/code]
Program.cs:
<pre class="prettyprint using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace ComboBoxBindingIssue
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

Mappings model = new Mappings();
model.Collection.Add(new Mapping(String.Empty, "Column A"));
model.Collection.Add(new Mapping(String.Empty, "Column B"));
model.Collection.Add(new Mapping(String.Empty, "Column C"));
model.Collection.Add(new Mapping(String.Empty, "Column D"));

model.LeftElements.Add(String.Empty);
model.LeftElements.Add("Column 1");
model.LeftElements.Add("Column 2");
model.LeftElements.Add("Column 3");
model.LeftElements.Add("Column 4");
model.LeftElements.Add("Column 5");

MappingDialog dialog = new MappingDialog();
dialog.Model = model;
DialogResult result = dialog.ShowDialog();
}
}
}
[/code]
<br/>


View the full article
 
Back
Top