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);
}
}
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.
Showing the filtered combo boxes in action.
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.
I am developing windows form using c# and using datagridview object. I am almost done but I have a problem with displaying item value to a specific column(PTS GAIN COLUMN) that I selected in a comboboxcell all inside datagridview. Data is selected from database(coded). The column(PTS GAIN COLUMN) where I want to display the selected item value has no entry in the database. It is empty. I want that every time I select a item from a comboboxcell per row is that it will display the value to a specific column(PTS GAIN COLUMN) and compute the total dynamically/real-time(I want to show the result in label.text)
Also the combobox cell has items YES,NO,NA(this has no datatable, I just added the items by coding combobox.items.add("yes/no/na"). Yes item will get value depending on the column PTS AVAIL and display on column PTS GAIN. If I select no, 0 will display in PTS GAIN column, and if NA, both PTS AVAIL and PTS GAIN will have 0. Again I want to if possible to compute the total real-time.
Any help with this matter is much appreciated. I am meeting a dead line so please, anyone! Have a great day! Btw, I will post screenshot of the program, and if you want to see a particular block of code for reference just comment.
You will need to hook up with 2 events to accomplish this but for the most part it is pretty easy.
private void MyInitializeComponent()
{
dg.CurrentCellDirtyStateChanged += Dg_CurrentCellDirtyStateChanged;
dg.CellValueChanged += Dg_CellValueChanged;
this.CalculateTotals();
}
private void Dg_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
if (dg.Columns[e.ColumnIndex].Name == "Choices")
{
switch (dg.Rows[e.RowIndex].Cells["Choices"].Value.ToString())
{
case ("Yes"):
{
dg.Rows[e.RowIndex].Cells["PointsGained"].Value =
dg.Rows[dg.CurrentCell.RowIndex].Cells["PointsAvailable"].Value;
break;
}
case ("No"):
{
dg.Rows[e.RowIndex].Cells["PointsGained"].Value = 0;
break;
}
case ("NA"):
{
dg.Rows[e.RowIndex].Cells["PointsGained"].Value = "NA";
break;
}
}
this.CalculateTotals();
}
}
private void Dg_CurrentCellDirtyStateChanged(object sender, EventArgs e)
{
if ((dg.Columns[dg.CurrentCell.ColumnIndex].Name == "Choices") &&
(dg.IsCurrentCellDirty))
{
dg.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
}
private void CalculateTotals()
{
var totalPointsGained = dg.Rows.Cast<DataGridViewRow>()
.Where(a => a.Cells["PointsGained"].Value?.ToString() != "NA")
.Sum(a => Convert.ToInt32(a.Cells["PointsGained"].Value));
var totalPointsAvailable = dg.Rows.Cast<DataGridViewRow>()
.Where(a => a.Cells["PointsAvailable"].Value?.ToString() != "NA")
.Sum(a => Convert.ToInt32(a.Cells["PointsAvailable"].Value));
lblTotalPointsGained.Text = "Total Points Gained: " + totalPointsGained;
lblTotalAvailable.Text = "Total Points Available: " + totalPointsAvailable;
}
You can put the code I have in MyInitializeComponent() wherever you initialize your other objects on the form.
Using Windows forms, My 'listview' have multiple columns as shown in the picture.
I have been trying to make this txtbox_search to be advanced. When any character, word or number is inserted, i want Some columns of my listview to be traversed to look for the character, word, number and bring up data related to the input.
Like when i enter: txtbox_search.Text = "a"
It should travers column "Name" and fill Listview with data:
entire row that has a name which starts with "a" such as "Anwar"
entire row that has a name which starts with "a" such as "Anas"
so on with entire rows that has a name which starts with "A..."
when i enter: txtbox_search.Text = "1"
It should travers column "ID" and fill Listview with data:
entire row that has a ID which starts with "1" such as "1002"
entire row that has a ID which starts with "1" such as "1112"
so on with entire rows that has a ID which starts with "1..."
so far i have been trying this for 2 days and end up with this much:
private void textBox_DEC_Search_TextChanged(object sender, EventArgs e)
{
foreach(ListViewItem Items in listView_DEC_CustomerList.Items)
{
if(Items.Text == textBox_DEC_Search.Text)
{
listView_DEC_CustomerList.Items.Clear();
listView_DEC_CustomerList.Items.Add(Items);
}
}
if(textBox_DEC_Search.Text == "" || textBox_DEC_Search.Text == string.Empty)
{
CusList Cus = new CusList();
Cus.CustomersList(listView_DEC_CustomerList);
}
}
This code only travers first column and bring up data that matches the inserted ID, only if the Complete ID matches with txtbox_search.Text how can i make this possible? (i want it to be on client side, not from sql/database). Guides and sample code helps will be really appreciated. Thanks.
To distinguish between your 2 criteria you could use the following:
if (textBox_DEC_Search.Text.All(x => Char.IsNumber(x)))
{
Debug.WriteLine("Number");
// search through ID
}
else
{
Debug.WriteLine("Name");
// search through Name
}
It basically checks whether your input is solely numeric.
EDIT:
To check for similarity you cold use String.StartsWith of String.Contains to make the search a little more flexible
to look for the ID or NAME you need to access the subitems!
since ID is your first column check SubItems[0]
if(Items.SubItems[0].Text.StartsWith(textBox_DEC_Search.Text) ||
Items.SubItems[0]Text.Contains(textBox_DEC_Search.Text))
since NAME is your second column check SubItems[1]
if(Items.SubItems[1].Text.StartsWith(textBox_DEC_Search.Text) ||
Items.SubItems[1]Text.Contains(textBox_DEC_Search.Text))
One Problem is this line:
listView_DEC_CustomerList.Items.Clear();
because it will erase the first found result when the second is found.
So if you find 10 matches the previous 9 will be deleted!
I suggest to make first the entire search and then add the results if there are any:
private void textBox_DEC_Search_TextChanged(object sender, EventArgs e)
{
// index is 0 if numeric for ID or 1 if not for NAME
int ind = textBox_DEC_Search.Text.All(x => Char.IsNumber(x)) ? 0 : 1;
List<ListViewItem> matchlist = new List<ListViewItem>();
foreach(ListViewItem Items in listView_DEC_CustomerList.Items)
{
if(Items.SubItems[ind].Text.StartsWith(textBox_DEC_Search.Text) ||
Items.SubItems[ind]Text.Contains(textBox_DEC_Search.Text))
{
matchlist.Add(Items);
}
}
// if you have found something add the all results
if(matchlist.Any())
{
listView_DEC_CustomerList.Items.Clear();
listView_DEC_CustomerList.Items.AddRange(matchlist.ToArray());
}
}
Disclaimer: Although this solution should work I would vote to follow the advice of #RezaAghaei. It is less messy and confusing than directly manipulating the ListView
Instead of using == which looks for an exact match try one of the following (I am assuming 'Text' is the column name in the list containing the name - if not change it to Items.Name (for example)
if you want to search on 'starting with' then try
if (Items.Text.StartsWith(textBox_DEC_Search.Text.Trim())
if you want to search based on the fact that a part of the string should be looked up then try
if (Items.Text.Contains(textBox_DEC_Search.Text.Trim())
You can similarly do for any other column you would like to search on. if you want to make the search case insensitive then use .ToLower() on the string and the column name.
Is there any possibility to skip or delete specific column of DataGridView from updating to the database?
I need to prevent column from updating to the database, because the column values is encrypted and when I decrypt, the decrypted values updated to the database.
I used this code before, but this really slows the grid.
private void dataGridView1_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
if (hide == false && e.ColumnIndex == 2 && e.RowIndex != this.dataGridView1.NewRowIndex)
{
e.Value = Decrypt(e.Value.ToString());
}
}
Updating values with this code:
private void dataGridView1_RowValidated(object sender, DataGridViewCellEventArgs e)
{
DataTable changes = ((DataTable)dataGridView1.DataSource).GetChanges();
if (changes != null)
{
MySqlCommandBuilder mcb = new MySqlCommandBuilder(mySqlDataAdapter);
((DataTable)dataGridView1.DataSource).AcceptChanges();
mySqlDataAdapter.UpdateCommand = mcb.GetUpdateCommand();
mySqlDataAdapter.Update(changes);
}
}
Based on what the title is saying, it's enough to set ReadOnly property of the column to true, but since you want to show decrypted value of the column in your grid, to prevent the column from being updated in database, you can use either of these options:
Show encrypted value in the column itself and change your update command to not contain statement for updating that specific column.
You can show the encrypted value in another unbound column.
In this post I show you an example of the second solution.
I suppose you have a string Decrypt(string value) method which decrypts an encrypted string. Also I can you have a column "A" which contains encrypted value and as the question you want to show decrypted value in a "B" column in grid.
So perform these steps:
Set Visible property of "A" column to false.
Add a DataGridViewTextBox column and set its name to "B"
Handle CellFormatting event of DtaGridView like below:
private void grid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
if(e.ColumnIndex<0 || e.RowIndex<0)
return;
var columnB = grid.Columns[e.ColumnIndex];
if (columnB.Name != "B")
return;
var value = grid.Rows[e.RowIndex].Cells["A"].Value;
if (value == null || value == DBNull.Value)
return;
cell.Value = Decrypt(value.ToString());
}
Note
It's not good idea to save changes on after you leave row. It's better to save changes after the edit is finished by click on a Save button.
You don't need to call AcceptChanges. In fact you should not!
Usually its enough to set cell.Value = Decrypt(value.ToString());but since you may have performance issues by Decrypt, Instead of setting e.Value you can check if the cell doesn't have value, will set the value for cell:
var cell = grid.Rows[e.RowIndex].Cells["B"];
if (cell.Value== null || cell.Value == DBNull.Value)
{
cell.Value = Decrypt(value.ToString());
}
Please pay attention to the warning which is in
remarks section of the CellFormatting event: CellFormatting event occurs every time each cell is painted, so
you should avoid lengthy processing when handling this event.
I'm reading a text file line by line, and inserting it into an array.
I then have this list called custIndex, which contains certain indices, indices of the items array that I'm testing to see if they are valid codes. (for example, custIndex[0]=7, so I check the value in items[7-1] to see if its valid, in the two dictionaries I have here). Then, if there's an invalid code, I add the line (the items array) to dataGridView1.
The thing is, some of the columns in dataGridView1 are Combo Box Columns, so the user can select a correct value. When I try adding the items array, I get an exception: "The following exception occurred in the DataGridView: System.ArgumentException: DataGridViewComboBoxCell value is not valid."
I know the combo box was added correctly with the correct data source, since if I just add a few items in the items array to the dataGridView1, like just items[0], the combo box shows up fine and there's no exception thrown. I guess the problem is when I try adding the incorrect value in the items array to the dataGridView1 row.
I'm not sure how to deal with this. Is there a way I can add all of the items in items except for that value? Or can I add the value from items and have it show up in the combo box cell, along with the populated drop down items?
if(choosenFile.Contains("Cust"))
{
var lines = File.ReadAllLines(path+"\\"+ choosenFile);
foreach (string line in lines)
{
errorCounter = 0;
string[] items = line.Split('\t').ToArray();
for (int i = 0; i <custIndex.Count; i++)
{
int index = custIndex[i];
/*Get the state and country codes from the files using the correct indices*/
Globals.Code = items[index - 1].ToUpper();
if (!CountryList.ContainsKey(Globals.Code) && !StateList.ContainsKey(Globals.Code))
{
errorCounter++;
dataGridView1.Rows.Add(items);
}
}//inner for
if (errorCounter == 0)
dataGridView2.Rows.Add(items);
}//inner for each
}//if file is a customer file
Say your text file contains:
Australia PNG, India Africa
Austria Bali Indonisia
France England,Scotland,Ireland Greenland
Germany Bahama Hawaii
Greece Columbia,Mexico,Peru Argentina
New Zealand Russia USA
And lets say your DataGridView is setup with 3 columns, the 2nd being a combobox.
When you populate the grid and incorrectly populate the combobox column you will get the error.
The way to solve it is by "handling/declaring explicitly" the DataError event and more importantly populating the combobox column correctly.
private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
//Cancelling doesn't make a difference, specifying the event avoids the prompt
e.Cancel = true;
}
private void dataGridView2_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
e.Cancel = true;
}
So imagine the 2nd column contained a dropdownlist of countries and the 1st & 3rd column contained text fields.
For the 1st and 3rd columns they are just strings so I create a class to represent each row:
public class CountryData
{
public string FirstCountry { get; set; }
public string ThirdCountry { get; set; }
}
For the 2nd column "Countries" combobox cell's I have created a separate class because I will bind it to the 2nd columns datasource.
public class MultiCountryData
{
public string[] SeceondCountryOption { get; set; }
}
Populating the grid with combobox columns and the like as shown here: https://stackoverflow.com/a/1292847/495455 is not good practice. You want to separate your business logic from your presentation for a more encapsulated, polymorphic and abstract approach that will ease unit testing and maintenance. Hence the DataBinding.
Here is the code:
namespace BusLogic
{
public class ProcessFiles
{
internal List<CountryData> CountryDataList = new List<CountryData>();
internal List<MultiCountryData> MultiCountryDataList = new List<MultiCountryData>();
internal void foo(string path,string choosenFile)
{
var custIndex = new List<int>();
//if (choosenFile.Contains("Cust"))
//{
var lines = File.ReadAllLines(path + "\\" + choosenFile);
foreach (string line in lines)
{
int errorCounter = 0;
string[] items = line.Split('\t');
//Put all your logic back here...
if (errorCounter == 0)
{
var countryData = new CountryData()
{
FirstCountry = items[0],
ThirdCountry = items[2]
};
countryDataList.Add(countryData);
multiCountryDataList.Add( new MultiCountryData() { SeceondCountryOption = items[1].Split(',')});
}
//}
}
}
}
In your presentation project here is the button click code:
imports BusLogic;
private void button1_Click(object sender, EventArgs e)
{
var pf = new ProcessFiles();
pf.foo(#"C:\temp","countries.txt");
dataGridView2.AutoGenerateColumns = false;
dataGridView2.DataSource = pf.CountryDataList;
multiCountryDataBindingSource.DataSource = pf.MultiCountryDataList;
}
I set dataGridView2.AutoGenerateColumns = false; because I have added the 3 columns during design time; 1st text column, 2nd combobox column and 3rd text column.
The trick with binding the 2nd combobox column is a BindingSource. In design time > right click on the DataGridView > choose Edit Columns > select the second column > choose DataSource > click Add Project DataSource > choose Object > then tick the multiCountry class and click Finish.
Also set the 1st column's DataPropertyName to FirstCountry and the 3rd column's DataPropertyName to ThirdCountry, so when you bind the data the mapping is done automatically.
Finally, dont forget to set the BindingSource's DataMember property to the multiCountry class's SeceondCountryOption member.
Here is a code demo http://temp-share.com/show/HKdPSzU1A