Using WinForms, I'm trying to write a method that checks if the data item bound to a row in a DataGridView contains an IList
as a property and then automatically transforms the DataGridViewTextBoxCell
into a DataGridViewComboBoxCell
binding that list as a datasource. The objective is to have a dropdown menu with different values for each row depending on the elements in the list property of the object found. So for example in the first row the dropdown could have 3 objects of type ObjA
as options, the second row could have 5 objects of type ObjC
, and so on.
This is what I have:
private void dgv_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
{
foreach (DataGridViewRow row in dgv.Rows)
{
object obj = row.DataBoundItem;
if (obj != null)
{
IEnumerable listProperties = obj.GetType().GetProperties().Where(p => p.GetValue(obj) is IList);
foreach (PropertyInfo list in listProperties)
{
DataGridViewComboBoxCell cell = new DataGridViewComboBoxCell();
IList source = (IList)list.GetValue(obj, null);
cell.DataSource = source;
cell.ValueMember = "id";
cell.DisplayMember = "name";
cell.ValueType = source.GetType().GetProperty("Item").PropertyType;
cell.Value = source[0].GetType().GetProperty("id").GetValue(source[0]);
(row.Cells[list.Name]) = cell;
}
}
}
}
And this is the code that binds the initial data to the DataGridView
:
IBindingList source = new BindingList<Models.InputValue>(datasource);
inputsDgv.AutoGenerateColumns = true;
inputsDgv.AllowUserToAddRows = false;
inputsDgv.AllowUserToDeleteRows = false;
inputsDgv.DataSource = source;
Problem: I'm getting a "DataGridViewComboBoxCell value is not valid" error when loading the DataGridView. I noticed that after running the line (row.Cells[list.Name]) = cell;
, the cell.ValueType
changes from System.Collections.Generic.IList1[[roco.Models.ISnapshots]]
to System.Int32
. I figured that must the the problem.
Does any one know how I can go around this error?
Thanks!
P.S.: The row.DataBoundItem
is of type InputValue
and the list property is a collection of ProjectionSnapshot
objects
public class InputValue : IValues
{
private int _id;
public int id { get { return _id; } }
private IList<ISnapshots> _snapshots;
public IList<ISnapshots> snapshots
{
get { return _snapshots; }
set { _snapshots = value; }
}
}
public class ProjectionSnapshot : ISnapshots
{
private int _id;
public int id { get { return _id; } }
private string _name;
public string name
{
get { return _name; }
set
{
if (value.Length > 255)
Console.WriteLine("Error! SKU snapshot name must be less than 256 characters!");
else
_name = value;
}
}
}
public interface ISnapshots
{
int id { get; }
string name { get; set; }
}
TL;DR: Skip to the Solution section.
Changing a DataGridViewTextBoxCell
into a DataGridViewComboBoxCell
is possible but will cause problems if the combo box values have a different type than the ValueType
set for the column. This is because, as mentioned by @Loathing and @Mohit Shrivastava, DataGridViewColumns
are tightly coupled with the corresponding Cell class.
After reading @Loathing's code example, I tried setting the Column's ValueType
to be typeof(Object)
before making alterations to the DataGridViewTextBoxCell
. This didn't work because when you bind an object to the DataGridView
and use AutoGenerateColumns
there is a mechanism that automatically sets the ValueType
of the Column to reflect the type of the property in the bound object. The same goes if you generate your own columns and set the Column's DataPropertyName
to an object property name.
Solution:
1) Generate your own columns mapping the object properties to the DataGridViewTextBoxCell
but not to the DataGridViewComboBoxCell
:
P.S.: I'm not checking for IList anymore, but for any IEnumerable (see this answer for more details: https://stackoverflow.com/a/9434921/5374324)
public void loadGrid<T>(IList<T> datasource)
{
generateDataGridViewColumns<T>(datasource);
IBindingList source = new BindingList<T>(datasource);
inputsDgv.AutoGenerateColumns = false;
inputsDgv.AllowUserToAddRows = false;
inputsDgv.AllowUserToDeleteRows = false;
inputsDgv.DataSource = source;
}
private void generateDataGridViewColumns<T>(IList<T> datasource)
{
dgv.Columns.Clear();
if (datasource != null)
{
foreach (PropertyInfo property in typeof(T).GetProperties())
{
DataGridViewColumn col;
var displayNameObj = property.GetCustomAttributes(typeof(DisplayNameAttribute), true).Cast<DisplayNameAttribute>().FirstOrDefault();
string displayName = (displayNameObj == null) ? property.Name : displayNameObj.DisplayName;
if (property.PropertyType.GetInterface(typeof(IEnumerable<>).FullName) != null && property.PropertyType != typeof(string))
{
col = new DataGridViewComboBoxColumn();
(col as DataGridViewComboBoxColumn).AutoComplete = false;
(col as DataGridViewComboBoxColumn).ValueType = typeof(Object);
}
else
{
col = new DataGridViewTextBoxColumn() { DataPropertyName = property.Name };
}
col.Name = property.Name;
col.HeaderText = displayName;
ReadOnlyAttribute attrib = Attribute.GetCustomAttribute(property, typeof(ReadOnlyAttribute)) as ReadOnlyAttribute;
col.ReadOnly = (!property.CanWrite || (attrib != null && attrib.IsReadOnly));
inputsDgv.Columns.Add(col);
}
}
}
2) Use the DataBindingComplete event to populate the combo boxes:
private void dgv_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
{
foreach (DataGridViewRow row in dgv.Rows)
{
object obj = row.DataBoundItem;
if (obj != null)
{
IEnumerable listProperties = obj.GetType().GetProperties().Where(p => p.GetValue(obj) is IList);
foreach (PropertyInfo list in listProperties)
{
IList source = (IList)list.GetValue(obj, null);
DataGridViewComboBoxCell cell = (row.Cells[list.Name] as DataGridViewComboBoxCell);
cell.DataSource = source;
cell.ValueType = source.GetType().GetProperty("Item").PropertyType;
ValueMember valueMember = (ValueMember)obj.GetType().GetProperty(list.Name).GetCustomAttribute(typeof(ValueMember));
DisplayMember displayMember = (DisplayMember)obj.GetType().GetProperty(list.Name).GetCustomAttribute(typeof(DisplayMember));
if(valueMember != null && displayMember != null)
{
cell.ValueMember = valueMember.Value;
cell.DisplayMember = displayMember.Value;
}
cell.Value = source[0].GetType().GetProperty("id").GetValue(source[0]);
}
}
}
}
3) Create the ValueMember and DisplayMember attribute classes:
[System.AttributeUsage(System.AttributeTargets.Property)]
public class ValueMember : System.Attribute
{
public string Value { get; private set; }
public ValueMember(string valueMember)
{
this.Value = valueMember;
}
}
[System.AttributeUsage(System.AttributeTargets.Property)]
public class DisplayMember : System.Attribute
{
public string Value { get; private set; }
public DisplayMember(string displayMember)
{
this.Value = displayMember;
}
}
4) Use the attributes:
public class InputValue
{
public string id{ get; set; }
public string name{ get; set; }
[DisplayName("Values")]
[ValueMember("id")]
[DisplayMember("name")]
public IList<IValue> values{ get; set; }
}