Search code examples
c#datagridviewcomboboxwindows-forms-designer

Load different data into different rows of the same comboboxcolumn c#


Okay, so I have a dataGridView with two combobox columns.

Bank and BankBranch, the user has the option to add several banks in the combobox but the branch list depends on the bank selected.

on the first row, this works flawlessly.

On any other rows, when the bank is selected, all the branch columns on all rows are updated to the branch list of that bank.

My question is, how do I make it so that when a second or third bank is selected, the branch list for only that row is updated and not all the others.

This is what im playing with.

if (grid.CurrentCell != null)
            {
                if (grid.CurrentCell.ColumnIndex == 3)
                {
                    if (grid.CurrentRow != null)
                    {
                        foreach (Bank bank in banks)
                        {
                            if (bank.Description.Trim() == grid.CurrentRow.Cells["gridBank"].Value.ToString().Trim())
                            {
                                bankID = bank.ID;
                                GetBankBranchList(grid.CurrentRow.Index);
                            }
                        }
                        
                    }
                }
            }

and this is the GetBankBranchList method

bankBranches = dal.GetByCriteria<BankBranch>(bankBranchQuery);
            foreach (BankBranch bankBranch in bankBranches)
            {
                if (bankBranch.Active)
                {
                    gridBranch.Items.Add(bankBranch.Description);
                }

            }

Solution

  • A common misconception is that a DataGridViewComboBoxCell works like a regular ComboBox. And in many ways, it does. However; a regular ComboBox is far more forgiving than a DataGridViewComboBoxCell. Example, in code, if you try to set a regular ComboBoxes value to something that is NOT in its list of items… then… nothing happens. No error/exception is thrown and the value is simply not set. This is NOT the case for the DataGridViewComboBoxCell. Setting a combo box cells value to something that is NOT in its list of items will cause the DataGridView to throw its DataError exception.

    You may reach for a last resort option by simply swallowing/ignoring the grids DataError, however, that is a poor choice for many reasons. In particular, in the case of the using the grid’s combo boxes as we want, you may not have the luxury of ignoring the grids DataError since the constant errors may eventually overwhelm the UI.

    One approach to creating Cascading Combo boxes in a DataGridView

    As others have commented, one possible solution for the combo box column that will have each combo box cell containing different values… is to set the combo box “columns” data source to a list that contains “ALL” the possible combo box values. Then, individually set each combo box “cell’s” DataSource to a “filtered/subset” list of the combo box column's DataSource. This is the approach used in the full example below.

    I used your Bank-Branch type scenario. Specifically, there are 10 different Banks and 50 different Branches. Each Bank may have zero or more Branches. In addition, a Branch may belong to more than one Bank, in other words, many banks may have the same Branch.

    To test, we will need a Customer. A Customer will have a unique ID, a Name, a BankID and possibly a BranchID. This will be used to test the combo boxes when the grid’s DataSource is set. However, we will break this down to two steps. The first step is to get the two combo boxes working properly FIRST and do NOT set the grids data source. After step 1 is complete, then we will move to step 2 which deals with the issues when setting the grids data source.

    You can follow along by creating a new winforms solution, drop a DataGridView, and Button onto the form, name the Button btnNewData and wire up its Click event. Copy the posted code below in the steps as shown below to finish with a form that works something like below…

    This shows the error message boxes when the data is loaded.

    enter image description here

    Showing the filtered combo boxes in action.

    enter image description here

    Step 1) Setting up the combo boxes without setting the grids data source.

    For this example, the grid will be set up to hold Customer data and the combo box columns would contain the Bank and Branch values for that Customer.

    To start, we are going to make a global “regular” ComboBox variable called SelectedCombo. When the user clicks on a Banks combo box cell the grids EditingControlShowing event will fire and we will cast that DataGridViewComboBoxCell to a “regular” ComboBox i.e., SelectedCombo. Then, subscribe to the SelectedIndexChanged event. When the SelectedIndexChanged event fires, we would then set the accompanying Branch cells data source.

    We will use several grid events, and below is a brief description of the events used in this code.

    EditingControlShowing event …

    Fires when the user clicks into a grid cell/combo box cell and puts that cell into “edit” mode. This event fires BEFORE the user actually types/changes anything in the cell. If the edited cell is a Banks combo box cell, then, the code sets up the global SelectedCombo variable and subscribes to its SelectedIndexChanged event.

    CellLeave event…

    Fires when the user tries to “leave” a cell. This event is only used to “un-subscribe” from the global variable SelectedCombo Combo Boxes SelectedIndexChanged event. Otherwise, the event will improperly fire when the user clicks on one of the Branches combo box cells.

    DefaultValuesNeeded event…

    Fires when the user types something or selects a combo box value in the LAST “new” row in the grid. This may cause some problems if the data source for the combo boxes does NOT have an “empty/null” value. So, the idea is to go ahead and give the “new” row of combo boxes some default values, namely the first Bank in the Banks list and an empty Branch.

    To help we will create three simple Classes.

    BranchCB Class

    Represents a Branch and has two properties… an int BranchID and string BranchName. And overriding the ToString() method for debugging output.

    In addition, there is a static BlankBranch property that returns a BranchCB object with a BranchID of 0 and an empty string as BranchName. NOTE: One possible issue using the DataGridViewComboBoxCell is when a cells value becomes empty/null. The grid may complain about this and throw its DataError. To help minimize this, AND to allow the Customer to have a “no” branch option, we will add a BlankBranch to the Branches combo box column’s data source and to each Bank’s Branch collection. Even if a Bank has “no” Branches, this BlankBranch will be present in the Bank’s Branches collection.

    public class BranchCB {
      public int BranchID { get; set; }
      public string BranchName { get; set; }
    
      public static BranchCB BlankBranch {
        get {
          return new BranchCB { BranchID = 0, BranchName = "" };
        }
      }
    
      public override string ToString() {
        return "BranchID: " + BranchID + " Name: " + BranchName;
      }
    }
    

    BankCB Class

    The BankCB class is straight forward, an int BankID, a string BankName and a BindingList of BranchCB objects. An overridden ToString() method for debugging.

    public class BankCB {
      public int BankID { get; set; }
      public string BankName { get; set; }
      public BindingList<BranchCB> Branches { get; set; }
    
      public override string ToString() {
        StringBuilder sb = new StringBuilder();
        sb.AppendLine("----------------------------------------------------------");
        sb.AppendLine("BankID: " + BankID + " Name: " + BankName + " Branches:...");
        if (Branches.Count > 1) {
          foreach (BranchCB branch in Branches) {
            if (branch.BranchID != 0) {
              sb.AppendLine(branch.ToString());
            }
          }
        }
        else {
          sb.AppendLine("No Branches");
        }
        return sb.ToString();
      }
    }
    

    Customer Class

    A Customer class with an int CustomerID, string CustomerName and two int properties for BankID and BranchID. This class is used for testing the combo boxes.

    public class Customer {
      public int CustomerID { get; set; }
      public string CustomerName { get; set; }
      public int BankID { get; set; }
      public int BranchID { get; set; }
    }
    

    For this example, five (5) global variables are created…

    Random rand = new Random(); 
    BindingList<BankCB> Banks;
    BindingList<BranchCB> Branches;
    BindingList<Customer> Customers;
    ComboBox SelectedCombo;
    

    rand is used for creating test data.

    Banks is a list of BankCB objects and will be used as a DataDource for the Banks DataGridViewComboBoxColumn.

    Branches is a list of ALL BranchCB objects and will be used as a DataSource for the Branches DataGridViewComboBoxColumn.

    Customers is a list of Customer objects and will be used as a DataSource for the DataGridView.

    Lastly, the SelectedCombo is a regular ComboBox and is used as previously described.

    Creating some test random Banks and Branches data…

    The code below creates and sets the global variables Banks and Branches variables with 50 Branches and 10 Banks. Each Bank will have 0 to max 10 Branches. Some Branches may be left out and may not be used by any Bank. Each Bank’s Branches list will not contain duplicate Branches; however, multiple Banks may have the same Branch.

    private void Setup_10_BanksWithRandomNumberOfBranches() {
      Branches = new BindingList<BranchCB>();
      Branches.Add(BranchCB.BlankBranch);
      for (int numOfBranches = 1; numOfBranches <= 50; numOfBranches++) {
        Branches.Add(new BranchCB { BranchID = numOfBranches, BranchName = "Branch " + numOfBranches });
      }
      Banks = new BindingList<BankCB>();
      BindingList<BranchCB> tempBranches;
      BranchCB curBranch;
      int totBranches;
      for (int numOfBank = 1; numOfBank <= 10; numOfBank++) {
        tempBranches = new BindingList<BranchCB>();
        tempBranches.Add(BranchCB.BlankBranch);
        totBranches = rand.Next(0, 11);
        for (int i = 0; i < totBranches; i++) {
          curBranch = Branches[rand.Next(0, 50)];
          if (!tempBranches.Contains(curBranch)) {
            tempBranches.Add(curBranch);
          }
        }
        tempBranches = new BindingList<BranchCB>(tempBranches.OrderBy(x => x.BranchID).ToList());
        Banks.Add(new BankCB { BankID = numOfBank, BankName = "Bank " + numOfBank, Branches = tempBranches });
      }
      foreach (BankCB bank in Banks) {
        Debug.WriteLine(bank);
      }
    }
    

    Adding the columns to the grid

    Setting the grids Banks combo box column should be fairly straight forward since all the combo boxes contain the same data. We want to set the Bank combo box column’s ValueMember property to the BankID and set its DisplayMember property to the BankName. For the Branches combo box column, the ValueMember would be BranchID and DisplayMember would be BranchName.

    private void AddColumns() {
      dataGridView1.Columns.Add(GetTextBoxColumn("CustomerID", "Customer ID", "CustomerID"));
      dataGridView1.Columns.Add(GetTextBoxColumn("CustomerName", "Customer Name", "CustomerName"));
      DataGridViewComboBoxColumn col = GetComboBoxColumn("BankID", "BankName", "BankID", "Banks", "Banks");
      col.DataSource = Banks;
      dataGridView1.Columns.Add(col);
      col = GetComboBoxColumn("BranchID", "BranchName", "BranchID", "Branches", "Branches");
      col.DataSource = Branches;
      dataGridView1.Columns.Add(col);
    }
    
    private DataGridViewComboBoxColumn GetComboBoxColumn(string dataPropertyName, string displayMember, string valueMember, string headerText, string name) {
      DataGridViewComboBoxColumn cbCol = new DataGridViewComboBoxColumn();
      cbCol.DataPropertyName = dataPropertyName;
      cbCol.DisplayMember = displayMember;
      cbCol.ValueMember = valueMember;
      cbCol.HeaderText = headerText;
      cbCol.Name = name;
      return cbCol;
    }
    
    private DataGridViewTextBoxColumn GetTextBoxColumn(string dataPropertyName, string headerText, string name) {
      DataGridViewTextBoxColumn txtCol = new DataGridViewTextBoxColumn();
      txtCol.DataPropertyName = dataPropertyName;
      txtCol.HeaderText = headerText;
      txtCol.Name = name;
      return txtCol;
    }
    

    At this time, we do not want to set the grids data source and want one row in the grid with the Bank and Branches combo boxes. If we run the code from the form’s load event…

    private void Form3_Load(object sender, EventArgs e) {
      Setup_10_BanksWithRandomNumberOfBranches();
      AddColumns();
    }
    

    We should see both Bank and Branch combo box cells and selecting the bank combo box should display 10 Banks while the Branches combo box will display all 50 Branches plus the “blank” branch.

    Filtering the Branches combo box cells

    The first grid event to subscribe to is the grids EditingControlShowing event. If the edited cell “is” a Bank combo box cell, then, we want to cast that Bank combo box cell to our global SelectedCombo ComboBox variable. Then have the global SelectedCombo subscribe to is SelectedIndexChanged event.

    dataGridView1.EditingControlShowing += new DataGridViewEditingControlShowingEventHandler(dataGridView1_EditingControlShowing);
    
    private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e) {
      if (dataGridView1.Columns[dataGridView1.CurrentCell.ColumnIndex].Name == "Banks") {
        SelectedCombo = e.Control as ComboBox;
        if (SelectedCombo != null) {
          SelectedCombo.SelectedIndexChanged -= new EventHandler(ComboBox_SelectedIndexChanged);
          SelectedCombo.SelectedIndexChanged += new EventHandler(ComboBox_SelectedIndexChanged);
        }
      }
    }
    

    We need to implement the ComboBox_SelectedIndexChanged event. When this event fires, we know the Bank selection has changed and we want to set the accompanying Branch cells data source. We can get the selected BankCB object from the global SelectedCombo.SelectedItem property. Next, we get the Branches combo box cell for that row and set is DataSource to the selected BankCB’s Branches collection. Lastly, set the Value of the Branches cell to the “blank” Branch which will always be the first “empty” Branch in the list.

    Even though we have changed the cells data source to a different list, we can have confidence, that each Bank’s Branches list is a “subset” of ALL the Branches used in the Branches combo box column’s DataSource. This should help in minimizing the chances of throwing the grid’s DataError.

    private void ComboBox_SelectedIndexChanged(object sender, EventArgs e) {
      if (SelectedCombo.SelectedValue != null) {
        BankCB selectedBank = (BankCB)SelectedCombo.SelectedItem;
        DataGridViewComboBoxCell branchCell = (DataGridViewComboBoxCell)(dataGridView1.CurrentRow.Cells["Branches"]);
        branchCell.DataSource = selectedBank.Branches;
        branchCell.Value = selectedBank.Branches[0].BranchID;
      }
    }
    

    If we run the code now… you should note that... BEFORE you click the Bank combo box cell… if you click on the Branch combo box cell then you will see “all” the Branches. However, if you select/change the Bank combo box value, then, click on the Branches combo box cell… you will get a casting error in our ComboBox_SelectedIndexChanges code.

    The problem is that the global ComboBoxes SelectedCombo’s SelectedIndexChanged event is still wired up and will fire when the Branches combo box is selected… which we do not want. We need to UN-subscribe the SelectedCombo from its SelectedIndexChanged event when the user “leaves” a Bank cell. Therefore, wiring up the grids CellLeave event and UN-subscribing from the global SelectedCombo_SelectedIndexChanged event should fix this.

    dataGridView1.CellLeave += new DataGridViewCellEventHandler(dataGridView1_CellLeave);
    
    private void dataGridView1_CellLeave(object sender, DataGridViewCellEventArgs e) {
      if (dataGridView1.Columns[e.ColumnIndex].Name == "Banks") {
        SelectedCombo.SelectedIndexChanged -= new EventHandler(ComboBox_SelectedIndexChanged);
      }
    }
    

    If we run the code now… the user can change the Branch combo box without errors. However, there is one possible issue… “Before” the user selects a Bank, the user can click on the Branches combo box and since the Bank combo box has not been set, the user will be shown “all” the branches and can select any Branch. This could possibly leave the Branch in an inconsistent state with a Branch selected but no Bank selected. We do not want this to happen.

    In this example, we will wire up the grids DefaultValuesNeeded event to set the new row to a “default” state by setting the BankID to the first bank in the Banks list. And set the new rows Branch value to the “Blank” Branch. This should take care of preventing the user from selecting a Branch without “first” selecting a Bank.

    dataGridView1.DefaultValuesNeeded += new DataGridViewRowEventHandler(dataGridView1_DefaultValuesNeeded);
    
    private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e) {
      int newCustID = 1;
      if (Customers != null) {
        newCustID = Customers.Count;
      }
      e.Row.Cells["CustomerID"].Value = newCustID;
      DataGridViewComboBoxCell cbCell = (DataGridViewComboBoxCell)e.Row.Cells["Banks"];
      cbCell.DataSource = Banks;
      cbCell.Value = Banks[0].BankID;
      cbCell = (DataGridViewComboBoxCell)e.Row.Cells["Branches"];
      cbCell.DataSource = Banks[0].Branches;
      cbCell.Value = Banks[0].Branches[0].BranchID;
    }
    

    This should now have the combo boxes working as we want without errors. If the user adds rows, the method above will help in avoiding an inconsistent Bank-Branch state. This should end the first step.

    Step 2) Adding a DatSource to the grid.

    We will need some test Customer data. For this test, Customer data will be 18 Customers. The first 15 will have valid Bank and Branch values. “Customer 16” will have an invalid Bank number, 17 will have an invalid Branch and lastly 18 will have a valid Bank and a valid Branch however, the Branch value will not be in the Branches collection for the Customers selected Bank.

    private BindingList<Customer> GetCustomers() {
      BindingList<Customer> customers = new BindingList<Customer>();
      BankCB curBank;
      BranchCB curBranchID;
      for (int i = 1; i <= 15; i++) {
        curBank = Banks[rand.Next(0, Banks.Count)];
        if (curBank.Branches.Count > 0) {
          curBranchID = curBank.Branches[rand.Next(0, curBank.Branches.Count)];
          customers.Add(new Customer { CustomerID = i, CustomerName = "Cust " + i, BankID = curBank.BankID, BranchID = curBranchID.BranchID });
        }
        else {
          customers.Add(new Customer { CustomerID = i, CustomerName = "Cust " + i, BankID = curBank.BankID, BranchID = BranchCB.BlankBranch.BranchID });
        }
      }
      customers.Add(new Customer { CustomerID = 16, CustomerName = "Bad Cust 16", BankID = 22, BranchID = 1 });
      customers.Add(new Customer { CustomerID = 17, CustomerName = "Bad Cust 17", BankID = 3, BranchID = 55 });
      customers.Add(new Customer { CustomerID = 18, CustomerName = "Bad Cust 18", BankID = 3, BranchID = 1 });
      return customers;
    }
    

    Updating the forms load event may look like…

    private void Form3_Load(object sender, EventArgs e) {
      dataGridView1.EditingControlShowing += new DataGridViewEditingControlShowingEventHandler(dataGridView1_EditingControlShowing);
      dataGridView1.CellLeave += new DataGridViewCellEventHandler(dataGridView1_CellLeave);
      dataGridView1.DefaultValuesNeeded += new DataGridViewRowEventHandler(dataGridView1_DefaultValuesNeeded);Setup_10_BanksWithRandomNumberOfBranches();
    
      Setup_10_BanksWithRandomNumberOfBranches();
      AddColumns();
      Customers = GetCustomers();
      dataGridView1.DataSource = Customers;
    }
    

    If you run this code, you should be getting the grid’s DataError attention at least twice for each bad Customer test data. When the grids data source is set you may see two of the bad Customers (16, and 17) such that the Bank or Branch combo boxes values are empty. If you roll the cursor over those two combo boxes, you should see the data error continually firing and basically we need to fix this. In addition , if you look at bad Customer 18, you will note that the Branch combo box value is set to an inconsistent state... in my particular test… Branch 1 is not a Branch in Bank 3.

    The reason for the errors is obvious, however, the solution not so much. In this case, we have a DataSource to the grid with BAD data for the combo boxes and unfortunately, we can NOT ignore this. We MUST do something. In this situation there is no “good” option, the data from the DB is obviously corrupt and we cannot continue without doing something. So…, you can leave the row out by removing it… Or … you could add the bad value as a new combo box value… Or … you could “change” the bad value to a default/good value. Other options may apply, but the bottom line is… we want to continue but we have to do something about the bad values first. So… Pick your own poison.

    In this example, I am going with the last option and will change the bad values to default/valid values, and popup a message box to the user to let them know what was changed then continue.

    We MUST check the combo box values in the Customer’s data BEFORE we set the grid’s DataSource. Therefore, A small method is created that loops through the Customers list and checks for bad Bank and bad Branch values. If a bad Bank value is found, then, the Bank value will be set to the first Bank in the Banks list. If the Bank value is ok, but the Branch is bad, then we will set the Branch to the “blank” Branch.

    This looks like a lot of code for this; however, most of the code is building the error string. You could simply change the values and continue and never interrupt the user. However, as a minimum a debug or log statement would be a good idea for debugging purposes.

    First a check is made to see if the BankID is valid, then the BranchID. If either is bad, then, the bad values will be replaced with default/valid values.

    Keep in mind… since each Branch combo box cell’s list of items is based on what Bank is selected, then, we need to look in the Customer’s selected Bank’s Branches collection and see if the Customers BranchID is one of the Branches in that Bank’s Branches collection.

    private void CheckDataForBadComboBoxValues() {
      StringBuilder sb = new StringBuilder();
      foreach (Customer cust in Customers) {
        sb.Clear();
        List<BankCB> targetBank = Banks.Where(x => x.BankID == cust.BankID).ToList();
        if (targetBank.Count > 0) {
          BankCB curBank = targetBank[0];
          var targetBranch = curBank.Branches.Where(x => x.BranchID == cust.BranchID).ToList();
          if (targetBranch.Count > 0) {
            sb.AppendLine("Valid bank and branch");
            Debug.Write(sb.ToString());
          }
          else {
            sb.AppendLine("Invalid Branch ID ----");
            sb.AppendLine("CutomerID: " + cust.CustomerID + " Name: " + cust.CustomerName);
            sb.AppendLine("BankID: " + cust.BankID + " BranchID: " + cust.BranchID);
            sb.AppendLine("Setting Bank to : " + cust.BankID + " setting branch to empty branch");
            MessageBox.Show(sb.ToString(), "Invalid Branch ID!", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            Debug.WriteLine(sb.ToString());
            if (curBank.Branches.Count > 0) {
              cust.BranchID = curBank.Branches[0].BranchID;
            }
          }
        }
        else {
          sb.AppendLine("Invalid Bank ID ----");
          sb.AppendLine("CutomerID: " + cust.CustomerID + " Name: " + cust.CustomerName);
          sb.AppendLine("BankID: " + cust.BankID + " BranchID: " + cust.BranchID);
          sb.AppendLine("Setting Bank to first bank, setting branch to empty branch");
          MessageBox.Show(sb.ToString(), "Invalid Bank ID!", MessageBoxButtons.OK, MessageBoxIcon.Warning);
          Debug.WriteLine(sb.ToString());
          cust.BankID = Banks[0].BankID;
          if (Banks[0].Branches.Count > 0) {
            cust.BranchID = Banks[0].Branches[0].BranchID;
          }
          else {
            cust.BranchID = BranchCB.BlankBranch.BranchID;
          }
        }
      }
    }
    

    Calling this method before we set the grids DataSource should eliminate the previous DataError. I highly recommend checking the grids DataSource values before setting it as a DataSource to the grid. Specifically, the combo box values simply to avoid a possible code crash. I have no faith in “good” data, so a check is needed just to CYA.

    Now the updated forms Load method may look something like…

    private void Form3_Load(object sender, EventArgs e) {
      dataGridView1.EditMode = DataGridViewEditMode.EditOnEnter;
      dataGridView1.EditingControlShowing += new DataGridViewEditingControlShowingEventHandler(dataGridView1_EditingControlShowing);
      dataGridView1.CellLeave += new DataGridViewCellEventHandler(dataGridView1_CellLeave);
      dataGridView1.DefaultValuesNeeded += new DataGridViewRowEventHandler(dataGridView1_DefaultValuesNeeded);
      Setup_10_BanksWithRandomNumberOfBranches();
      AddColumns();
      Customers = GetCustomers();
      CheckDataForBadComboBoxValues();
      dataGridView1.DataSource = Customers;
    }
    

    This should get rid of the grid’s DataError when setting the grids data source. In addition, the bad Branch value for Customer 18 is set to a blank Branch as we want. However, there is still one small issue… when the grids data source was set, the previous actions/work we did by using the grids’ events to manage each Bank-Branch combo box cell… did not happen when the grids DataSource was set. When each Customer row was added to the grid, the events we used earlier to set each Branch combo box cell’s DataSource did not fire.

    The Branch combo box will display the proper selected Customer’s Branch value in the combo box, however, if you click on a Branch combo box, you will see all the branches since the individual Branch combo box cells DataSource has not been set yet.

    So, we need another method to loop through the grid’s row collection, grab the rows selected Bank, then set the Branch combo box cell’s DataSource to the selected Banks Branches collection. We only need to do this once after the grids data source has been set.

    private void SetAllBranchComboCellsDataSource() {
      Customer curCust;
      foreach (DataGridViewRow row in dataGridView1.Rows) {
        if (!row.IsNewRow) {
          curCust = (Customer)row.DataBoundItem;
          BankCB bank = (BankCB)Banks.Where(x => x.BankID == curCust.BankID).FirstOrDefault();
          // since we already checked for valid Bank values, we know the bank id is a valid bank id
          DataGridViewComboBoxCell cbCell = (DataGridViewComboBoxCell)row.Cells["Branches"];
          cbCell.DataSource = bank.Branches;
        }
      }
    }
    

    After this change the grids FINAL updated Load method may look something like below. Setting the grids EditMode to EditOnEnter will facilitate clicking on the combo box cells once to get the drop down to display. In addition a Button is added to the form to re-set the grids data for testing.

    Random rand = new Random(); 
    BindingList<BankCB> Banks;
    BindingList<BranchCB> Branches;
    BindingList<Customer> Customers;
    ComboBox SelectedCombo;
    
    private void Form3_Load(object sender, EventArgs e) {
      dataGridView1.EditMode = DataGridViewEditMode.EditOnEnter;
      dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
      dataGridView1.EditingControlShowing += new DataGridViewEditingControlShowingEventHandler(dataGridView1_EditingControlShowing);
      dataGridView1.CellLeave += new DataGridViewCellEventHandler(dataGridView1_CellLeave);
      dataGridView1.DefaultValuesNeeded += new DataGridViewRowEventHandler(dataGridView1_DefaultValuesNeeded);
      SetNewData();
    }
    
    private void SetNewData() {
      dataGridView1.Columns.Clear();
      Setup_10_BanksWithRandomNumberOfBranches();
      //Setup_10_BanksWith5BranchesNoDuplicates();
      AddColumns();
      Customers = GetCustomers();
      CheckDataForBadComboBoxValues();
      dataGridView1.DataSource = Customers;
      SetAllBranchComboCellsDataSource();
    }
    
    private void btnNewData_Click(object sender, EventArgs e) {
      SetNewData();
    }
    

    This should complete the example. However, it may be a challenge to test some aspects of this when the number of branches is randomly generated along with the random branches selected. In other words, different data is produced each time the code is executed. To remove this randomness, I created a second set of data such that there are 10 Banks and 50 Branches. Each Bank has exactly five (5) Branches. In addition, each Branch belongs to one and only one bank. Bank 1 has Branches 1-5; Bank 2 has Branches 6-10, etc. and all 50 Branches are used only once. For testing, it may be easier using this data.

    Call this method instead of the Setup_10_BanksWithRandomNumberOfBranches();

    private void Setup_10_BanksWith5BranchesNoDuplicates() {
      Branches = new BindingList<BranchCB>();
      Branches.Add(BranchCB.BlankBranch);
      for (int numOfBranches = 1; numOfBranches <= 50; numOfBranches++) {
        Branches.Add(new BranchCB { BranchID = numOfBranches, BranchName = "Branch " + numOfBranches });
      }
      Banks = new BindingList<BankCB>();
      BindingList<BranchCB> tempBranches;
      BranchCB curBranch;
      int branchIndex = 1;
      for (int numOfBank = 1; numOfBank <= 10; numOfBank++) {
        tempBranches = new BindingList<BranchCB>();
        tempBranches.Add(BranchCB.BlankBranch);
        for (int i = 0; i < 5; i++) {
          if (branchIndex < Branches.Count) {
            curBranch = Branches[branchIndex++];
            tempBranches.Add(curBranch);
          }
          else {
            break;
          }
        }
        tempBranches = new BindingList<BranchCB>(tempBranches.OrderBy(x => x.BranchID).ToList());
        Banks.Add(new BankCB { BankID = numOfBank, BankName = "Bank " + numOfBank, Branches = tempBranches });
      }
    }
    

    Sorry for the long post. I hope this makes sense and helps.