Wednesday, September 27, 2006

Problems with the DataGridViewComboBoxColumn

I've been playing with the new DataGridView that comes with Windows.Forms 2.0. It's really nice because it fully supports in-place editing and supplies a range of built in cell editors. Unfotunately they don't behave exactly as you'd expect (or maybe it's just me) and I've just spent a very fustrating moring with much googling trying to get some simple functionality to work that should have been a ten minute job. The problem is with the combo box cell editor (the DataGridViewComboBoxColumn column type). Now, with a standard windows combo box you can assign its DataSource property to anything that implements IList, by default it displays the ToString() value in the drop down list and the SelectedItem property is the item that's selected, as you'd expect. This makes it really really easy (one line of code easy) to get the user to choose a particular object from a list. There are also DisplayMember and ValueMember properties that allow you to set the property that's displayed and the property that is returned from SelectedItem, but I've never used these, since the default behaviour is exactly what I need 99% of the time.

However, the DataGridViewComboBoxColumn doesn't work like this, although it will display the ToString value if you don't set the DisplayMember, something internally goes wrong when it tries to look up the SelectedItem, you have to set DisplayMember to a public property of your class. Even worse, the default behaviour if you don't set the ValueMember property is to return the DisplayMember, there's no way of getting actual item itself. The only work around is to add a property to your class that returns itself and set that property to the ValueMember. Of course, if your item isn't something you are able to change (such as one of the framework classes) you'll have to cludge together a container object that holds a reference to your item. It's really awkward. Here's a little example I used to try and get it to work...

First, here's the class I want to bind to my DataGridView, note that the Child property is of type Child:

using System;

namespace DataGridViewTest
{
    public class Thing
    {
        string _name;

        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }

        Child _child;

        public Child Child
        {
            get { return _child; }
            set { _child = value; }
        }
    }
}

Here's the Child class, you can see that I've implemented a 'This' propety that returns a reference to itself:

using System;

namespace DataGridViewTest
{
    public class Child
    {
        string _name;

        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }

        public Child(string name)
        {
            _name = name;
        }

        public Child This
        {
            get { return this; }
        }
    }
}

And here's the form code. In the contructor we bind a list of things to the DataGridView using a P&P DataGridViewBinding, set the _childColumn.ValueMember to the This property so that it returns a child to the Child property of Thing, we set the _childColumn.DisplayMember to the Name property of child to display in the drop down list because the default ToString() functionality doesn't work and finally bind the DataSource of the child column to a list of children. showToolStripMenuItem_Click is the event handler for a menu item that just displays the things that have been created and GetChildren simply constructs a list of children.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Microsoft.Practices.SoftwareFactories.ServiceFactory.Editors.Grid;

namespace DataGridViewTest
{
    public partial class Form1 : Form
    {
        List _things = new List();
        DataGridViewBinding _thingsBinding = new DataGridViewBinding();

        public Form1()
        {
            InitializeComponent();
            _childColumn.ValueMember = "This";
            _childColumn.DisplayMember = "Name";

            _thingsBinding.Bind(_dataGrid, _things);
            _childColumn.DataSource = GetChildren();
        }

        private void showToolStripMenuItem_Click(object sender, EventArgs e)
        {
            StringBuilder message = new StringBuilder();
            foreach(Thing thing in _things)
            {
                message.Append(string.Format("{0}, {1}\r\n", thing.Name, thing.Child.Name));
            }
            MessageBox.Show(message.ToString());
        }

        private Child[] GetChildren()
        {
            return new Child[]{
                new Child("freddy"),
                new Child("Alice"),
                new Child("Leo"),
                new Child("Ben")
            };
        }
    }
}

I noticed that all the example code in MSDN is devoted to binding the DataGridView to DataSets, I wonder if it's because of this data set centric mind set that the Windows.Forms team is making such basic implementation errors?

10 comments:

Anonymous said...

I agree 110%, the dataset centric thinking of the Windows Forms team is making some programming much harder. What ever happened to the days when I (as a developer) knew what I wanted to do with my data, be it from a database or other source, and I told the application how to work. Now my environment is setting my workflow and this is starting to drive me insane!!!!

Anonymous said...

Thank you so much for taking the time to write this down, I've been looking for a solution to this problem for weeks (if not even months!).
Keep up the good work!

Mike Hadlow said...

Thanks Andreas, glad you found it useful.

Anonymous said...

firefox has a pretty sweet spell checker built right in!

Mike Hadlow said...

Yes yes, I know my spelling is horrendous and I'm now a big fan of the firefox spell checker. I was a victim of a cruel sixties education experiment called ITA which tried to improve early reading skills by inventing a new phonetic alphabet for English. Nice idea guys, shame the only books written in it are yours.

LowOrbit said...

Thanks for this - it was frustrating to find that the SelectedItem property is missing from the DataGridViewComboBoxCell. Your solution solves the problem nicely.

Unknown said...

Thanks you guy, this terrible DataGridViewComboBoxColumn binding is really silly... Now I hope it will be much better for me!

Anonymous said...

Well i recently get into this trouble and your solution seems the correct way (i saw another one extending the dgvcomboboxcolumn control), one thing to mention, is that only seems to work when the datasource is an array a validation error is thrown from the dgv if you try to use a binding list as the datasource for the column; i know this and old entry but just my 2 cents in case anyone else is having the same problem :D

Anonymous said...

C# build GridView ComboBox column

李安柏 said...

I know this is an old issue, but oh my goodness, thank you!