Sunday, December 12, 2010

DataGridViewComboBoxColumn: enum databinding

DataGridViewComboBoxColumn is most complex column type in DataGridView control and one needs little more work to establish data binding. The common way of DataGridView column binding is to set DataSource and DataPropertyName. That is, set data source (list data source or item data source) to DataSource property of DataGridView control and then set DataPropertyName for each column. For example, below code has <MyData> class which has 3 properties - Name, Region, Check. For Name (DataGridViewTextBoxColumn) and Check (DataGridViewCheckBoxColumn) properties, all we need is to set DataPropertyName for each column and DataSource for DataGridView control. However, for Region (DataGridViewComboBoxColumn), we need to set all possible combo item values - typically by setting DataGridViewComboBoxColumn.DataSource property. (please note that this DataSource is different from DataGridView.DataSource)

What happens if one doesn't specify all possible values in DataGridViewComboBoxColumn? Well, most likely one will see the following exception: "System.ArgumentException: DataGridViewComboBoxCell value is not valid."

Before moving to the topic of how to set DataGridViewComboBoxColumn properties, it's worthy of mentioning the order of property setting. One always wants to set DataPropertyName and other column properties before setting DataGridView.DataSource. Otherwise, it's likely one will see no data displayed in the grid or the exception above (DataGridViewComboBoxCell value is not valid) for combobox column.

Now for DataGridViewComboBoxColumn type, if one wants to display bare enum values in the combobox, all it takes is to set DataPropertyName to enum property. (and of course DataGridView.DataSource, Method1 below demonstrates it.) However, if one wants to display some description or localized string for enum values, combo column's DataSource typically should hold 2 values (enum value and its description (or localized string)) Below source code (method2) demonstrates enum data binding with its description. In summary, (1) DataSource in DataGridViewComboBoxColumn should be set to list data source of enum-description pairs. (2) DisplayMember is the property name for string description property of the combo data source (3) ValueMember is the enum type property name of the combo data source.
public partial class Form1 : Form
    {
        List<MyData> dataList;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            // For Name column (DataGridViewTextBoxColumn type)
            // or Check (DataGridViewCheckBoxColumn),
            // simply set DataPropertyName (and dataGridView.DataSource)
            this.NameColumn.DataPropertyName = "Name";
            this.CheckColumn.DataPropertyName = "Check";

            // For Region column (DataGridViewComboBoxColumn type), set DataPropertyName
            // and all possible items in combobox
            this.RegionColumn.DataPropertyName = "Region";
            // (Method1) Use enum directly. Set combo.DataSource only.
            //           Cons: No description or localized string for enum.
            //
            //BindingList<RegionType> regTypes = new BindingList<RegionType>();
            //foreach (RegionType rt in Enum.GetValues(typeof(RegionType)))
            //{
            //    regTypes.Add(rt);
            //}
            //this.RegionColumn.DataSource = regTypes;

            // (Method2) Use description string in combobox.
            //           Set combo.DataSource/DisplayMember/ValueMember
            //
            BindingList<RegionDescType> locRegion = new BindingList<RegionDescType>();
            foreach (RegionType rt in Enum.GetValues(typeof(RegionType)))
            {
                locRegion.Add(new RegionDescType(rt));
            }
            this.RegionColumn.DataSource = locRegion;
            this.RegionColumn.DisplayMember = "RegionName";
            this.RegionColumn.ValueMember = "RegionType";           
            // Sample data
            dataList = new List<MyData>() {
                new MyData("Steve" , RegionType.America, true),
                new MyData("Kim", RegionType.Asia, false),
                new MyData("Claude", RegionType.Europe, true)
            };
            // Bind data to datagridview control
            // dataGridView.DataSource should be set after all Column bindings are set.
            BindingSource bindSrc = new BindingSource();
            bindSrc.DataSource = dataList;
            this.dataGridView1.DataSource = bindSrc;
        }
    }

    public class MyData
    {
        public string Name { get; set; }
        public RegionType Region { get; set; }
        public bool Check { get; set; }
        public MyData(string name, RegionType region, bool check)
        {
            this.Name = name;
            this.Region = region;
            this.Check = check;
        }
    }

    public enum RegionType
    {               
        [Description("America region")]
        America,       
        [Description("Asia region")]
        Asia,      
        [Description("Europe/Mideast")]
        Europe,       
        [Description("Africa region")]
        Africa
    }

    public class RegionDescType
    {
        private RegionType regType;
        public string RegionName { get; private set; }
        public RegionType RegionType { get { return regType; } }
        public RegionDescType(RegionType type)
        {
            regType = type;
            RegionName = GetDescription();
            //RegionName = GetLocString();
        }

        private string GetLocString()  //assuming Resource1.resx has loc strings
        {
            return Resource1.ResourceManager.GetString(regType.ToString());
        }
        private string GetDescription()
        {
            FieldInfo fi = typeof(RegionType).GetField(regType.ToString());
            DescriptionAttribute[] descAttrs = fi.GetCustomAttributes(typeof(DescriptionAttribute), false)
                                   as DescriptionAttribute[];
            if (descAttrs.Length > 0)
            {
                return descAttrs[0].Description;
            }
            return regType.ToString();
        }
    }
One more thing. Often time we want to have localized strings for enum values, rather than fixed enum descriptions. I showed an example of implementing how to get loc string above(GetLocString()). But typically this is implemented by using TypeConverter for enum type. TypeConverter provides more sophisticated way, it can handle which type(s) can be acceptable for type conversion and supports both direction conversion by ConvertTo / ConvertFrom methods (I guess it's a separate topic)