Trying to store a list in a Data Grid - c#

I am trying to make a program that has a data grid that shows on each row, List of Ingredients the pizza has, the pizza name, and the price of the pizza. I can get the data grid to show the name and price, but i am having trouble getting it to show the list of ingredients. The data grid's datasource is a binding list of a class called Pizza.
class Pizza
{
private List<Ingredients> ingredientList_;
private string pizzaName_;
private decimal retailPrice_;
public Pizza(List<Ingredients> ingredientList, string pizzaName, decimal retailPrice)
{
ingredientList_ = ingredientList;
pizzaName_ = pizzaName;
retailPrice_ = retailPrice;
}
It has the basic get and set properties.
I also have an Ingredient class.
class Ingredients
{
private string name_;
private int servingSize_;
private int energyValue_;
private decimal purchasePrice_;
private bool isVegetarian_;
public Ingredients(string name, int servingSize, int energyValue, decimal purchasePrice, bool isVegetarian)
{
name_ = name;
servingSize_ = servingSize;
energyValue_ = energyValue;
purchasePrice_ = purchasePrice;
isVegetarian_ = isVegetarian;
}
Has the basic get and set properties.
In my form code, I have:
private BindingList<Pizza> pizzaList_;
pizzaList_ = new BindingList<Pizza>();
dataGridViewPizzaMenu.DataSource = pizzaList_;
Now my problem is that i am trying to use a combo box column to show the ingredients in a pizza when i click on it. But i can't seem to create a bound column for the Ingredients, only the pizza name and pizza price. Am i missing something or is what i am trying to do not possible?

Everything you've done appears correct. The problem I think is the way the DataGridView is setup at Design or Runtime.
If you head over to this answer you can see the steps you need to take:
Add all elements of array to datagridview rows except one
The trick with binding the 1st combobox column is a BindingSource. In design time > right click on the DataGridView > choose Edit Columns > select the first column > choose DataSource > click Add Project DataSource > choose Object > then tick the Ingredients class and click Finish.
Remeber to set the 1st ComboBox columns DataMember to ingredientList, you will need to select the IngredientsDataBindingSource control that was added (slightly below the Form Design's surface - in the gray area)
2nd and 3rd add two TextBox columns for pizzaName and retailPrice and set there DataPropertyName accordingly.
pizzaList_ = new BindingList<Pizza>();
//Insert code to populate the List of Pizza's and Ingredients
dataGridViewPizzaMenu.AutoGenerateColumns = false;
dataGridViewPizzaMenu.DataSource = pizzaList_;
ingredientsDataBindingSource.DataSource = pizzaList_.ingredientsList_;
ps There is a downloadable sample in the link I referred to above.

You can make a comboBox and set this event, next, with Datagridview1.Row.Add method insert a row in Datagridview. But before it, you should a DatagridviewRow with some cells, that own cell of this row is DataGrifViewComboBoxCell.
Good Luck...

In order to put combo box to third field you have to create first two as a datagridview items:
DataGridViewRow RowSample = new DataGridViewRow();
DataGridViewComboBoxCell pizzaItem = new DataGridViewComboBoxCell();
pizzaItem.DataSource = pizzaList_;
pizzaItem.Value = pizzaList_[0];
DataGridViewCell pizzaName = new DataGridViewTextBoxCell();
pizzaName.Value = pizza.pizzaName; // creating the text cell
DataGridViewCell pizzaPrice = new DataGridViewTextBoxCell();
pizzaPrice.Value = pizza.pizzaPrice;; // creating the text cell
RowSample.Cells.Add(pizzaName);
RowSample.Cells.Add(pizzaPrice);
RowSample.Cells.Add(pizzaItem);
SampleGridView.Rows.Add(RowSample);
Now RowSample added to you datagridview with 3 field, and the third one is combobox.

On the form load event
ingredientList_ = context.Ingredients.OrderBy(p => p.ingredientName).ToList();
FillCombo();
//Create a method to fill combo
private void FillCombo()
{
IngredientBindingSource.DataSource = ingredientList_;
}

Related

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);
}
}
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.

Setting value for DataGridViewRow.Tag when data binding by DataSource for DatagridView

I am trying to set a value for DataGridViewRow.Tag when data binding but I don't know how to do it?
I tried with DataRow row = table.NewRow(); But row doesn't have tag.
How to set a value for DataGridViewRow.Tag when binding DataGridView to table (for example)? Or it isn't possible?
Edit1:
Here is the code i am using:
var table = new DataTable();
table.Columns.Add("Title", typeof(string));
table.Columns.Add("URL", typeof(string));
table.Columns.Add("Read Later", typeof(bool));
foreach (XElement node in nodes)
{
Helper.CheckNode(node);
var id = node.Attribute("id").Value;
var url = node.Element("path").Value;
var comment = node.Element("title").Value;
var readlater = node.Attribute("readLater")?.Value.ToString() == "1";
var row = table.NewRow();
row.ItemArray = new object[] { url, comment, readlater };
table.Rows.Add(row);//Edit2
}
dataGridView1.DataSource = table;
I am trying to set a tag for the row to use it in CellClick event:
var cRow = dataGridView1.Rows[e.RowIndex];
var id = cRow.Tag.ToString();
Separate the data from how it is displayed
When using a DataGridView, it is seldom a good idea to access the cells and the columns directly. It is way more easier to use DataGridView.DataSource.
In modern programming there is a tendency to separate your data (= model) from the way your data is displayed (= view). To glue these two items together an adapter class is needed, which is usually called the viewmodel. Abbreviated these three items are called MVVM.
Use DataGridView.DataSource to display the data
Apparently, if the operator clicks a cell, you want to read the value of the tag of the row of the cell, to get some extra information, some Id.
This displayed row is the display of some data. Apparently part of the functionality of this data is access to this Id. You should not put this information in the view, you should put it in the model.
class MyWebPage // TODO: invent proper identifier
{
public int Id {get; set;}
public string Title {get; set;}
public string Url {get; set;}
public bool ReadLater {get; set;}
... // other properties
}
Apparently you have a method to fetch the data that you want to display from a sequence of nodes. Separate fetching this data (=model) from displaying it (= view):
IEnumerable<MyWebPage> FetchWebPages(...)
{
...
foreach (XElement node in nodes)
{
Helper.CheckNode(node);
bool readLater = this.CreateReadLater(node);
yield return new MyWebPage
{
Id = node.Attribute("id").Value,
Url = node.Element("path").Value,
Title = node.Element("title").Value,
ReadLater = this.CreateReadLater(node),
};
}
}
I don't know what is in node "ReadLater", apparently you know how to convert it to a Boolean.
bool CreateReadLater(XElement node)
{
// TODO: implement, if null return true; if not null ...
// out of scope of this question
}
For every property that you want to display you create a DataGridViewColumn. Property DataPropertyName defines which property should be shown in the column. Use DefaultCellStyle if a standard ToString is not enough to display the value properly, for instance, to define the number of digits after the decimal point, or to color negative values red.
You can do this using the visual studio designer, or you can do this in the constructor:
public MyForm
{
InitializeComponents();
this.dataGridViewColumnTitle.DataPropertyName = nameof(MyWebPage.Title);
this.dataGridViewColumnUrl.DataPropertyName = nameof(MyWebPage.Url);
...
}
You don't want to display the Id, so there is no column for this.
Now to display the data, all you have to do is assign the list to the datasource:
this.dataGrieViewWebPages.DataSource = this.FetchWebPages().ToList();
This is display only. If the operator can change the displayed values, and you want to access the changed values, you should put the items in an object that implements interface IBindingList, for instance, using class (surprise!) BindingList<T>:
private BindingList<MyWebPage> DisplayedWebPages
{
get => (BindingList<MyWebPage>)this.dataGrieViewWebPages.DataSource;
set => this.dataGrieViewWebPages.DataSource = value;
}
Initialization:
private void DisplayWebPages()
{
this.DisplayedWebPages = new BindingList<MyWebPage>(this.FetchWebPages.ToList());
}
And presto! All webpages are displayed. Every change that the operator makes: add / remove / edit rows are automatically updated in the DisplayedWebPages.
If you want to access the currently selected WebPages:
private MyWebPage CurrentWebPage =>(MyWebPage)this.dataGrieViewWebPages.CurrentRow?.DataBoundItem;
private IEnumerable<MyWebPage> SelectedWebPages =>
this.dataGrieViewWebPages.SelectedRows
.Cast<DataGridViewRow>()
.Select(row => row.DataBoundItem)
.Cast<MyWebPage>();
Now apparently whenever the operator clicks a cell, you want to do something with the Id of the WebPage that is displayed in the Row of the cell.
View: Displayed Cell and Row
ViewModel: React when operator clicks a cell
Model Action that must be done
React on Cell Click: get the Id
We've handled the View above. ViewModel is the event handler:
void OnDatGridViewCellClicked(object sender, DataGridViewCellEventArgs e)
{
// use the eventArgs to fetch the row, and thus the WebPage:
MyWebPage webPage = (MyWebPage)this.dataGridViewWebPages.Rows[e.RowIndow].DataBoundItem;
this.ProcessWebPage(webPage);
}
ProcessWebPage is typically a method in your Model class:
public void ProcessWebPage(MyWebPage webPage)
{
// Do what you need to do if the operator clicks the cell, for example:
int webPageId = webPage.Id;
...
}
Conclusion: advantages of separating model from view
By the way, did you see that all ViewModel methods are one-liners? Only your Model methods FetchWebPages and ProcessWebPage contain several lines.
Because you separated the View from the Model, changes to your Model or your View will be fairly simple:
If you want to store your data in Json format, or in a database instead of in an XML, your View won't change
If you don't want to react on cell click, but on Button OK click, then your Model won't change. Your model also doesn't have to change if you decide to show more or less columns
Because you separated your Model from your View, the Model can be unit tested without a form. You can also test the View with a Model filled with only test values.

Set DataGridComboBoxColumn selected item

I'm trying to set a specific item as selectedItem in a DataGridComboBoxColumn. However a lot of research, I couln't find the right answer for me yet.
My scenario:
I have a programatically created a DataGrid which has an ObservableCollection<> as ItemsSource. As a last column, I want to add a DataGridComboBoxColumn to give the user a selection to choose from. Since such data can already be stored in the database, I need to "preset" the value from the collection stored in the database.
private void ManipulateColumns(DataGrid grid)
{
...
DataGridComboBoxColumn currencies = new DataGridComboBoxColumn();
//Here come the possible choices from the database
ObservableCollection<string> allCurrencies = new ObservableCollection<string>(Data.AllCurrencys);
currencies.ItemsSource = allCurrencies;
currencies.Header = "Currency";
currencies.CanUserReorder = false;
currencies.CanUserResize = false;
currencies.CanUserSort = false;
grid.Columns.Add(currencies);
currencies.MinWidth = 100;
//Set the selectedItem here for the column "Currency"
...
}
I found many tutorials for setting the selected item for normal ComboBoxes, but not for DataGridComboBoxColumns. I already tried it with currencies.SetCurrentValue(), but I can't find a suitable DependencyProperty from DataGridComboBoxColumn.
Can someone please help me out?
Thanks in advance.
Boldi
Building a DataGrid like that with C# code is messy. You should really take a look at using Data Binding instead. If you want to continue with building it in C#, then you are going to have to set the value per Row. There isn't a way to set a default for all the rows in a column. Say my DataGrid is bound to a collection of type Book. I can use the DataGrid SelectedItem property to get the Book object for the selected row, then set it's currency property. You're going to have to figure out what row you need to set the value for, get your object for that row, then set its currency property. This isn't a complete answer but it will get you started. Essentially, you are going to have to set it for each item in the DataGrid, not the column.
public class Book
{
public decimal price;
public string title;
public string author;
public string currency;
}
private void ManipulateColumns(DataGrid grid)
{
DataGridComboBoxColumn currencies = new DataGridComboBoxColumn();
//Here come the possible choices from the database
System.Collections.ObjectModel.ObservableCollection<string> allCurrencies = new System.Collections.ObjectModel.ObservableCollection<string>();
allCurrencies.Add("US");
allCurrencies.Add("asdf");
allCurrencies.Add("zzz");
currencies.ItemsSource = allCurrencies;
currencies.Header = "Currency";
currencies.CanUserReorder = false;
currencies.CanUserResize = false;
currencies.CanUserSort = false;
grid.Columns.Add(currencies);
currencies.MinWidth = 100;
//Set the selectedItem here for the column "Currency"
//currencies.
((Book)grid.SelectedItem).currency = "US Dollar";
}

Add all elements of array to datagridview rows except one

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

Add 2 or more field to value member of C# coding

I have table with 4 primary key fields. I load that in to drop down list in my WinForm application created by using C#.
On the TextChanged event of drop down list I have certain TextBox and I want to fill the information recived by the table for the certain field I selected by the drop down list.
So as I say the table having 4 fields. Can I get those all 4 fields into value member from the data set, or could you please tell me whether is that not possible?
Thank you.
Datatable dt=dba.getName();
cmb_name.ValueMember="id";
cmb_name.DisplayMember="name";
cmb_name.DataSource=dt;
this is normal format.. but i have more key fields.. so i need to add more key fields..
You can use DataSource property to bind your source data to the ComboBox (e.g. a List of Entities, or a DataTable, etc), and then set the DisplayMember property of the ComboBox to the (string) name of the field you want to display.
After the user has selected an Item, you can then cast the SelectedItem back to the original row data type (Entity, DataRow, etc - it will still be the same type as you put in), and then you can retrieve your 4 composite keys to the original item.
This way you avoid the SelectedValue problem entirely.
Edit:
Populate as follows:
cmb_name.DisplayMember = "name";
cmb_name.DataSource = dt;
// Ignore ValueMember and Selected Value entirely
When you want to retrieve the selected item
var selectedRow = (cmb_name.SelectedItem as DataRowView );
Now you can retrieve the 4 values of your PK, e.g. selectedRow["field1"], selectedRow["field2"], selectedRow["field3"] etc
If however you mean that you want to DISPLAY 4 columns to the user (i.e. nothing to do with your Table Key), then see here How do I bind a ComboBox so the displaymember is concat of 2 fields of source datatable?
cmb_name.DisplayMember = "name";
cmb_name.DataSource = dt;
DataRowView selectedRow = (cmb_name.SelectedItem as DataRowView );
The result will be here:
MessageBox.Show(selectedRow.Row[0].ToString());
MessageBox.Show(selectedRow.Row[1].ToString());
MessageBox.Show(selectedRow.Row[2].ToString());
MessageBox.Show(selectedRow.Row[3].ToString());
.....
If you want to get some data from a ComboBox in to a List you can use something like this
List<string> ListOfComboData = new List<string>();
ListOfComboData = yourComboBox.Items.OfType<string>().ToList<string>();
I have no real idea if this is what you mean as the question is very poorly structured. I hope this helps...
Edit: To put the selected text in to some TextBox use
yourTextBox.Text = youComboBox.Text;
in the SelectedIndexChanged event of your ComboBox.
You could follow the approach here with the following class:
public class ComboBoxItem
{
public string Text { get; set; }
public object[] PrimaryKey { get; set; }
}
private void Test()
{
ComboboxItem item = new ComboboxItem();
item.Text = "Item text1";
item.PrimaryKey = new object[] { primaryKey1, primaryKey2, primaryKey3, primaryKey4};
comboBox1.Items.Add(item);
comboBox1.SelectedIndex = 0;
MessageBox.Show((comboBox1.SelectedItem as ComboboxItem).Value.ToString());
}

Categories