Search code examples
c#winformsdatagridviewdatagridviewcomboboxdatabound

Bound DataGridView: the DataGridViewComboBoxColumn changes to a different value when I click outside it


I hope someone can help me out with an obscure problem that has me absolutely flummoxed.

I wish to set up a DataGridView, which allows a user to select an option from a DataGridViewComboBoxColumn, and for the object which is a datasource for the DataGridView, to be updated with the object that the user pick in the ComboBox.

[NB I wish the DataGridViewComboBoxColumn to show more than a single property in the dropdown, hence why I am using a DataTable as the DataGridViewComboBoxColumn datasource. In other words, I want them to see a description which is a combination of other properties concatenated together.]

My code works, but when I click outside the ComboBox cell, the value gets automatically set (it gets set back to the first item in BindingList). I cannot get it to stick to the value that the user selected. Ultimately the wrong object instance gets assigned to the DataGridView's dataSource.

I would post a pic, but I don't have enough rep.

So my question is, why does the DataGridViewComboBoxColumn switch to the first item in its datasource (a DataTable) when I click outside the cell. How can I get it to keep the option I selected, in the cell. I am absolutely BAFFLED.

I hope it's okay to post this much code up to the StackOverflow website without it annoying everyone. I've tried to create suitably generic example to help any other souls trying to find out how to select and assign an object to the property of another object, using a DataGridViewComboBoxColumn. So hopefully this is of use to someone else. I've also tried to make it relatively easy to recreate, should someone need to solve this kind of problem.

I've tried all manner of things, including using a List as a datasource of the DataGridViewComboBoxColumn, doing things with the CurrentCellDirtyStateChanged event - all to no avail.

Here's my 2 classes:

    public class CarPartChoice
    {
        public string Name { get; set; }
        public int Value { get; set; }
        public string Comment {get; set;}

        public CarPartChoice(string name, int value, string comment)
        {
            Name = name;
            Value = value;
            Comment = comment;
        }
    }

    public class Car
    {
        public string Manufacturer { get; set; }
        public string Model { get; set; }
        public CarPartChoice WheelChoice {get; set;}

        public Car(string maker, string model, CarPartChoice wheel)
        {
            Manufacturer = maker;
            Model = model;
            WheelChoice = wheel;
        }

        public Car(string maker, string model)
        {
            Manufacturer = maker;
            Model = model;
        }
    }

    public static class GlobalVariables
    {
        public static BindingList<CarPartChoice> GlobalChoiceList { get; set; }
        public static BindingList<Car> GlobalCarsList { get; set; }
    }

And here's me creating my dataGridView:

        private void Form1_Load(object sender, EventArgs e)
        {
            //Setup the wheel choices to be selected from the DataGridViewComboBoxColumn.
            CarPartChoice myWheelChoice = new CarPartChoice("Chrome", 19, "This is the chromes wheels option.");
            CarPartChoice myWheelChoice2 = new CarPartChoice("HubCaps", 16, "This is the nasty plastic hubcaps option.");
            BindingList<CarPartChoice> tempBLChoice = new BindingList<CarPartChoice>();
            tempBLChoice.Add(myWheelChoice);
            tempBLChoice.Add(myWheelChoice2);
            GlobalVariables.GlobalChoiceList = tempBLChoice;

            //Setup the cars to populate the datagridview.
            Car car1 = new Car("Vauxhall", "Astra");
            Car car2 = new Car("Mercedes", "S-class");
            BindingList<Car> tempListCars = new BindingList<Car>();
            tempListCars.Add(car1);
            tempListCars.Add(car2);
            GlobalVariables.GlobalCarsList = tempListCars;

            dataGridView1.AutoGenerateColumns = false;
            dataGridView1.CurrentCellDirtyStateChanged += new EventHandler(dataGridView1_CurrentCellDirtyStateChanged);

            // Set up 2 DataGridViewTextBox columns, one to show the manufacturer and the other to show the model.
            DataGridViewTextBoxColumn manufacturer_col = new DataGridViewTextBoxColumn();
            manufacturer_col.DataPropertyName = "Manufacturer";
            manufacturer_col.Name = "Manufacturer";
            manufacturer_col.HeaderText = "Manufacturer";
            DataGridViewTextBoxColumn model_col = new DataGridViewTextBoxColumn();
            model_col.DataPropertyName = "Model";
            model_col.Name = "Model";
            model_col.HeaderText = "Model";

            // Create a DataTable to hold the Wheel options available for the user to choose from. This DT will be the DataSource for the 
            //  ...combobox column
            DataTable wheelChoices = new DataTable();
            DataColumn choice = new DataColumn("Choice", typeof(CarPartChoice));
            DataColumn choiceDescription = new DataColumn("Description", typeof(String));
            wheelChoices.Columns.Add(choice);
            wheelChoices.Columns.Add(choiceDescription);
            foreach (CarPartChoice wheelchoice in GlobalVariables.GlobalChoiceList)
            {
                wheelChoices.Rows.Add(wheelchoice, wheelchoice.Name + " - " + wheelchoice.Value.ToString() + " - " + wheelchoice.Comment);
            }

            // Create the Combobox column, populated with the wheel options so that user can pick one.
            DataGridViewComboBoxColumn wheelOption_col = new DataGridViewComboBoxColumn();
            wheelOption_col.DataPropertyName = "WheelChoice";
            wheelOption_col.Name = "WheelChoice";
            wheelOption_col.DataSource = wheelChoices;
            wheelOption_col.ValueMember = "Choice";
            wheelOption_col.DisplayMember = "Description";
            wheelOption_col.ValueType = typeof(CarPartChoice);

            // Add the columns and set the datasource for the DGV.
            dataGridView1.Columns.Add(manufacturer_col);
            dataGridView1.Columns.Add(model_col);
            dataGridView1.Columns.Add(wheelOption_col);
            dataGridView1.DataSource = GlobalVariables.GlobalCarsList;
        }

UPDATE: I have tried to use a "BindingSource" object and setting the datasource of the BindingSource to the DataTable. That made no difference. I've also tried getting the Car to implement the "INotifyPropertyChanged" interface and that didn't make any difference.

The one thing if I've noticed is that the Car item in the datagridview DOES get its WheelChoice property updated. The Car objects IS INDEED updated with the value I've selected from the ComboBox. I think this is a just a display issue and the DataGridViewComboBoxColumn just isn't populated with the correct value. Still baffled.


Solution

  • Thanks to those who left comments. I found the answer in the end.

    The answer is for me simply to define an overridden "ToString()" method for my "CarPartChoice" class (i.e. the class which is used to provide the items to be looked-up by clicking on the ComboBoxColumn). I guess it was having trouble formatting my cell with a nice string when I moved control away from it.

    So, for anyone who ever wants to do this in future, here's a full example of how you might use a datagridview to update a list of objects of a certain class, and using a DataGridViewComboBoxColumn to provide the user with a choice of objects, and for their selected object (which they chose from the dropdown), to be populated into the list (to be precise: in the field of an object in the list which is of the type of object which gets selected by the user in the dropdown). Yes, I know that's a very horrible sentence.

    Here's the complete code which would accomplish this task (I would have thought it was something that people would often want to do).

    Kindest regards to all.

        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void Form1_Load(object sender, EventArgs e)
            {
                //Setup the wheel choices to be selected from the DataGridViewComboBoxColumn.
                CarPartChoice myWheelChoice = new CarPartChoice("Chrome", 19, "This is the chromes wheels option.");
                CarPartChoice myWheelChoice2 = new CarPartChoice("HubCaps", 16, "This is the nasty plastic hubcaps option.");
                CarPartChoice myWheelChoice3 = new CarPartChoice("Iron", 15, "These are metal wheels.");
                CarPartChoice myWheelChoice4 = new CarPartChoice("Spoked", 15, "This is the fancy classic hubcaps option.");
                CarPartChoice myWheelChoice5 = new CarPartChoice("solid", 13, "This wheels has no spokes or holes.");
                CarPartChoice myWheelChoice6 = new CarPartChoice("SpaceHubCaps", 17, "Newly developed space hubcaps.");
    
                BindingList<CarPartChoice> tempBLChoice = new BindingList<CarPartChoice>();
                tempBLChoice.Add(myWheelChoice);
                tempBLChoice.Add(myWheelChoice2);
                tempBLChoice.Add(myWheelChoice3);
                tempBLChoice.Add(myWheelChoice4);
                tempBLChoice.Add(myWheelChoice5);
                tempBLChoice.Add(myWheelChoice6);
    
                GlobalVariables.GlobalChoiceList = tempBLChoice;
    
                //Setup the cars to populate the datagridview.
                Car car1 = new Car("Vauxhall", "Astra");
                Car car2 = new Car("Mercedes", "S-class");
                BindingList<Car> tempListCars = new BindingList<Car>();
                tempListCars.Add(car1);
                tempListCars.Add(car2);
                GlobalVariables.GlobalCarsList = tempListCars;
    
                dataGridView1.AutoGenerateColumns = false;
                dataGridView1.CurrentCellDirtyStateChanged += new EventHandler(dataGridView1_CurrentCellDirtyStateChanged);
    
    
                // Set up 2 DataGridViewTextBox columns, one to show the manufacturer and the other to show the model.
                DataGridViewTextBoxColumn manufacturer_col = new DataGridViewTextBoxColumn();
                manufacturer_col.DataPropertyName = "Manufacturer";
                manufacturer_col.Name = "Manufacturer";
                manufacturer_col.HeaderText = "Manufacturer";
                DataGridViewTextBoxColumn model_col = new DataGridViewTextBoxColumn();
                model_col.DataPropertyName = "Model";
                model_col.Name = "Model";
                model_col.HeaderText = "Model";
    
                // Create a DataTable to hold the Wheel options available for the user to choose from. This DT will be the DataSource for the 
                //  ...combobox column
                DataTable wheelChoices = new DataTable();
                DataColumn choice = new DataColumn("Choice", typeof(CarPartChoice));
                DataColumn choiceDescription = new DataColumn("Description", typeof(String));
                wheelChoices.Columns.Add(choice);
                wheelChoices.Columns.Add(choiceDescription);
    
                foreach (CarPartChoice wheelchoice in GlobalVariables.GlobalChoiceList)
                {
                    wheelChoices.Rows.Add(wheelchoice, wheelchoice.Name + " - " + wheelchoice.Value.ToString() + " - " + wheelchoice.Comment);
                }
    
                // Create the Combobox column, populated with the wheel options so that user can pick one.
                DataGridViewComboBoxColumn wheelOption_col = new DataGridViewComboBoxColumn();
                wheelOption_col.DataPropertyName = "WheelChoice";
                wheelOption_col.DataSource = wheelChoices;
                wheelOption_col.ValueMember = "Choice";
                wheelOption_col.DisplayMember = "Description";
                wheelOption_col.ValueType = typeof(CarPartChoice);
    
                // Add the columns and set the datasource for the DGV.
                dataGridView1.Columns.Add(manufacturer_col);
                dataGridView1.Columns.Add(model_col);
                dataGridView1.Columns.Add(wheelOption_col);
                dataGridView1.DataSource = GlobalVariables.GlobalCarsList;
            }
    
            void dataGridView1_CurrentCellDirtyStateChanged(object sender, EventArgs e)
            {
                var grid = sender as DataGridView;
                if (grid.IsCurrentCellDirty)
                    grid.CommitEdit(DataGridViewDataErrorContexts.Commit);
            }
        }
    
        public class CarPartChoice
        {
            public string Name { get; set; }
            public int Value { get; set; }
            public string Comment { get; set; }
    
            public CarPartChoice(string name, int value, string comment)
            {
                Name = name;
                Value = value;
                Comment = comment;
            }
    
            public override string ToString()
            {
                return Name.ToString() + " - " + Value.ToString() + " - " + Comment.ToString();
            }
        }
    
        public class Car
        {
            public string Manufacturer { get; set; }
            public string Model {get; set; }
            public CarPartChoice WheelChoice { get; set; } 
    
            public Car(string maker, string model, CarPartChoice wheel)
            {
                Manufacturer = maker;
                Model = model;
                WheelChoice = wheel;
            }
    
            public Car(string maker, string model)
            {
                Manufacturer = maker;
                Model = model;
            }
        }
    
        public static class GlobalVariables
        {
            public static BindingList<CarPartChoice> GlobalChoiceList { get; set; }
            public static BindingList<Car> GlobalCarsList { get; set; }
        }