C#/WinForms: Binding a SortedList to a DataGridView

I ran into an issue the other day, and I didn’t find a suitable answer online. Maybe my google-fu was weak that day. Regardless, I thought I’d add my workaround here, in case anyone else runs into the same problem and wanders this way.

It’s fairly easy to bind a collection to a DataGridView in Windows Forms. First, you need a collection:

class MyClass
{
   public string PropertyA { get; set; }
   public string PropertyB { get; set; }
}

List<MyClass> myList;

In the form designer, add a DataGridView control, then add a couple of columns to it. In the properties for each column, set the DataPropertyName field to the name of the property you want the column to display. In this example, I set the first column to PropertyA and the second column to PropertyB. The design code looks like this:

//
// dataGridView1
//
this.dataGridView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((
     System.Windows.Forms.AnchorStyles.Top 
   | System.Windows.Forms.AnchorStyles.Bottom)
   | System.Windows.Forms.AnchorStyles.Left)
   | System.Windows.Forms.AnchorStyles.Right)));
this.dataGridView1.ColumnHeadersHeightSizeMode = 
     System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
   this.Column1,
   this.Column2});
this.dataGridView1.Location = new System.Drawing.Point(13, 13);
this.dataGridView1.Name = "dataGridView1";
this.dataGridView1.Size = new System.Drawing.Size(592, 377);
this.dataGridView1.TabIndex = 0;
//
// Column1
//
this.Column1.DataPropertyName = "PropertyA";
this.Column1.HeaderText = "A";
this.Column1.Name = "Column1";
//
// Column2
//
this.Column2.DataPropertyName = "PropertyB";
this.Column2.HeaderText = "B";
this.Column2.Name = "Column2";

Then, once you’ve created a list of your objects, attach it to the DataGridView:

dataGridView1.DataSource = myList;

And when the program runs, you should see your DataGridView populated with the data from your list.

In my case, rather than just a List collection, I had a SortedList<int, MyClass> collection that I wanted to display. You can’t just point the DataGridView.DataSource field at the SortedList, because you’ll get nothing. Same if you try SortedList.Values. The general advice I found for this was to use a BindingSource object:

var bindingSource = new BindingSource();
bindingSource.DataSource = myList.Values;
dataGridView1.DataSource = bindingSource;

And at first, this looked like it was working perfectly. One more gotcha – my collection didn’t just contain MyClass objects, it could contain various derivatives of MyClass. It was more like this:

class MyBase
{
   protected string a = "base a";
   virtual public string PropertyA { get { return a; } set { a = value; } }
   protected string b = "base b";
   virtual public string PropertyB { get { return b; } set { b = value; } }
}

class Deriv1 : MyBase
{
   public Deriv1( string x, string y)
   {
      a = "1: " + x;
      b = "1: " + y;
   }
}

class Deriv2 : MyBase
{
   public Deriv2( string x, string y )
   {
      a = "2: " + x;
      b = "2: " + y;
   }
}

SortedList<int, MyBase> myList;

...
   list = new SortedList<int, MyBase>( );
   list.Add( 1, new Deriv1( "aaa", "bbb" ) );
   list.Add( 3, new Deriv1( "ddd", "eee" ) );
   list.Add( 2, new Deriv2( "ggg", "hhh" ) );
   list.Add( 4, new Deriv2( "jjj", "kkk" ) );

   var bindingSource = new BindingSource();
   bindingSource.DataSource = list.Values;
...

The BindingSource works fine when the SortedList contains only instances of Deriv1, but as soon as I add a Deriv2 object, the binding starts throwing exceptions:


System.ArgumentException occurred
HResult=-2147024809
Message=The value "GridViewTest.Form1+Deriv2" is not of type "GridViewTest.Form1+Deriv1" and cannot be used in this generic collection.
Parameter name: value
Source=mscorlib
ParamName=value
StackTrace:
at System.ThrowHelper.ThrowWrongValueTypeArgumentException(Object value, Type targetType)
at System.Collections.ObjectModel.Collection`1.System.Collections.IList.Add(Object value)
at System.Windows.Forms.BindingSource.GetListFromEnumerable(IEnumerable enumerable)
at System.Windows.Forms.BindingSource.ResetList()
at System.Windows.Forms.BindingSource.set_DataSource(Object value)
at GridViewTest.Form1..ctor()
InnerException:

It seems to assume the type of the first object in the list, rather than the base object that I hoped/expected. Instead, I abandoned the BindingSource object approach, and instead made my own derivative of the SortedList collection that implemented the parts of the IBindingList interface that I needed:

public class BindableSortedList : SortedList<int, MyBase>, IBindingList
{
   public void Remove(object value)
   {
      // SortedList.Remove() expects an integer key, while IBindingList.Remove() expects a MyBase object
      if (value is MyBase)
         base.RemoveAt( base.IndexOfValue( value ) );
      else
         base.Remove((int)value); 
   }

   new public object this[int index]
   {
      get { return Values[index]; }
      set { throw new NotImplementedException(); }
   }
   public MyBase GetBySortedValue(int index) { return base[index]; }

   // Parts of IBindingList I've just left unimplemented for now
   public int Add(object value) { throw new NotImplementedException(); }
   public bool Contains(object value) { throw new NotImplementedException(); }
   public int IndexOf(object value) { throw new NotImplementedException(); }
   public void Insert(int index, object value) { throw new NotImplementedException(); }
   public object AddNew() { throw new NotImplementedException(); }
   public void AddIndex(PropertyDescriptor property) { throw new NotImplementedException(); }
   public void ApplySort(PropertyDescriptor property, ListSortDirection direction) { throw new NotImplementedException(); }
   public int Find(PropertyDescriptor property, object key) { throw new NotImplementedException(); }
   public void RemoveIndex(PropertyDescriptor property) { throw new NotImplementedException(); }
   public void RemoveSort() { throw new NotImplementedException(); }
   public bool IsReadOnly { get; private set; }
   public bool IsFixedSize { get; private set; }
   public bool AllowNew { get; private set; }
   public bool AllowEdit { get; private set; }
   public bool AllowRemove { get; private set; }
   public bool SupportsChangeNotification { get; private set; }
   public bool SupportsSearching { get; private set; }
   public bool SupportsSorting { get; private set; }
   public bool IsSorted { get; private set; }
   public PropertyDescriptor SortProperty { get; private set; }
   public ListSortDirection SortDirection { get; private set; }
   public event ListChangedEventHandler ListChanged;
}
BindableSortedList myList;

The biggest change is that IBindingList expects this[n] to return the nth entry, whereas this[n] on a SortedList returns the value added with Add(n, value). Here, I’m changing the indexer to return the nth value in the list, and adding a GetBySortedValue(n) method to replace the existing myList[n] operations in the code. As myList[n] now returns type ‘object’ instead of ‘MyBase’, the compiler/intellisense should point out most if not all of the required changes.

Now you can point the DataGridView directly at the collection without the need for an intermediate BindingSource object:

dataGridView1.DataSource = myList;

In my case, I was only using the DataGridView to view and edit entries in the collection, rather than having the DataGridView modify the collection itself. Other code was making changes to the collection, then I forcing the UI to reflect the changes by rebinding the collection:

dataGridView1.DataSource = null;
dataGridView1.DataSource = myList;

Leave a Reply