I am using a DataGridView
in a C#
WinForms
application and want to let the user choose between a set of values that will be the same in each cell. I suppose there needs to be a way to populate all the cells with a list of values.
Using the GUI designer, I have added COL_BUSINESS_INDUSTRY
, which is a DataGridViewComboBoxColumn
to the DataGridView. In the constructor, I want to populate the ComboBoxes with a list of values, for which I am using the following code:
COL_BUSINESS_INDUSTRY.DataSource = LoadIndustries();
COL_BUSINESS_INDUSTRY.DataPropertyName = "industryName";
COL_BUSINESS_INDUSTRY.DisplayMember = COL_BUSINESS_INDUSTRY.DataPropertyName;
COL_BUSINESS_INDUSTRY.ValueMember = COL_BUSINESS_INDUSTRY.DataPropertyName;
LoadIndustries()
will return a DataTable
that has two fiels: id
and industryName
The problem is that when I execute this, the ComboBoxes contain no values.
I have also tried having LoadIndustries()
return a List<string>
, without setting the DataPropertyName
, but that didn't work either.
I also tried doing this, with LoadIndustries()
returning a List<string>
, but still no luck:
COL_BUSINESS_INDUSTRY.Items.AddRange();
Obviously I'm doing something wrong but it's been over 3 hours of looking online and I have no idea. Why are my ComboBoxes not displaying the values?
EDIT: Following TaW's suggestion, I've made the following changes, which still don't work:
Added the following class to contain the values:
class IndustryObj
{
public string theString { get; set; }
public string theId { get; set; }
public IndustryObj(string id, string s) { theId = id; theString = s; }
public override string ToString() { return theString; }
}
I am loading the industries in the following method. I can guarantee that loading the values in the objects is working fine:
private static List<IndustryObj> LoadIndustries()
{
var industries = new List<IndustryObj>();
const string sql = "SELECT id, name FROM Industry ORDER BY name";
using (var sqlQuery = new CustomSqlQuery(MyDatabases.Telemarketing, sql))
{
foreach (var record in sqlQuery)
{
industries.Add(new IndustryObj(record[0].ToString(), record[1].ToString()));
}
}
return industries;
}
This is the method where I am populating the DataGridView. All values are loading fine, except for the ones in COL_BUSINESS_INDUSTRY
:
private void LoadBusinesses()
{
COL_BUSINESS_INDUSTRY.DataSource = LoadIndustries();
COL_BUSINESS_INDUSTRY.DataPropertyName = "theString";
COL_BUSINESS_INDUSTRY.DisplayMember = COL_BUSINESS_INDUSTRY.DataPropertyName;
COL_BUSINESS_INDUSTRY.ValueMember = "theId";
const string sql = "SELECT id, company, contact, address, phone, email, status, industry, callbackDate, callbackTime, createdBy, lastUpdatedBy, lockedUntil FROM Business ORDER BY company";
using (var sqlQuery = new CustomSqlQuery(MyDatabases.Telemarketing, sql))
{
var columns = new DataGridViewColumn[]
{
COL_BUSINESS_COMPANY,
COL_BUSINESS_CONTACT,
COL_BUSINESS_ADDRESS,
COL_BUSINESS_PHONE,
COL_BUSINESS_EMAIL,
COL_BUSINESS_STATUS,
COL_BUSINESS_INDUSTRY,
COL_BUSINESS_CALLBACKDATE,
COL_BUSINESS_CALLBACKTIME,
COL_BUSINESS_CREATEDBY,
COL_BUSINESS_LASTUPDATEDBY,
COL_BUSINESS_LOCKEDUNTIL
};
foreach (var record in sqlQuery)
{
dgv_Businesses.Rows.Add(CustomDGVRow.InitializeFromDataReader(true, record, columns));
}
}
}
EDIT 2: Following TaW's second suggestion, I've done the following. This did not help either:
private List<IndustryObj> industryObjects;
private void LoadBusinesses()
{
industryObjects = LoadIndustries();
COL_BUSINESS_INDUSTRY.DataSource = industryObjects;
COL_BUSINESS_INDUSTRY.DataPropertyName = "theString";
COL_BUSINESS_INDUSTRY.DisplayMember = COL_BUSINESS_INDUSTRY.DataPropertyName;
COL_BUSINESS_INDUSTRY.ValueMember = "theId";
...
EDIT 3: Luminous
actually got it, but that introduces a number of other problems. The loading method now looks like this:
private void LoadBusinesses()
{
// Load the records.
const string sql =
"SELECT id, company, contact, address, phone, email, status, industry, callbackDate, callbackTime, createdBy, lastUpdatedBy, lockedUntil FROM Business ORDER BY company";
using (var sqlQuery = new CustomSqlQuery(MyDatabases.Telemarketing, sql))
{
var columns = new DataGridViewColumn[]
{
COL_BUSINESS_COMPANY,
COL_BUSINESS_CONTACT,
COL_BUSINESS_ADDRESS,
COL_BUSINESS_PHONE,
COL_BUSINESS_EMAIL,
COL_BUSINESS_STATUS,
COL_BUSINESS_INDUSTRY,
COL_BUSINESS_CALLBACKDATE,
COL_BUSINESS_CALLBACKTIME,
COL_BUSINESS_CREATEDBY,
COL_BUSINESS_LASTUPDATEDBY,
COL_BUSINESS_LOCKEDUNTIL
};
foreach (var record in sqlQuery)
{
dgv_Businesses.Rows.Add(CustomDGVRow.InitializeFromDataReader(true, record, columns));
}
}
// Load the industries and bind the industry DataSource to the industry ComboBoxColumn.
COL_BUSINESS_INDUSTRY.DataSource = LoadIndustries();
COL_BUSINESS_INDUSTRY.DataPropertyName = "theString";
COL_BUSINESS_INDUSTRY.DisplayMember = COL_BUSINESS_INDUSTRY.DataPropertyName;
COL_BUSINESS_INDUSTRY.ValueMember = "theId";
// Select the correct industries in the industry column.
foreach (var rawRow in dgv_Businesses.Rows)
{
var row = rawRow as CustomDGVRow;
if (row == null) continue;
foreach (var item in ((CustomDGVComboBoxCell) row.Cells[COL_BUSINESS_INDUSTRY.Index]).Items)
{
var industry = item as IndustryObj;
if (industry == null) continue;
if (row.Cells[COL_BUSINESS_INDUSTRY.Index].Value != null && industry.theId == row.Cells[COL_BUSINESS_INDUSTRY.Index].Value.ToString())
{
row.Cells[COL_BUSINESS_INDUSTRY.Index].Value = industry;
}
}
}
}
As you can see, I also need to select one value from the list of items, based on its id in the database. Because I only bind the datasource after loading the items (as per Luminous
's suggestion), I need to do another pass through the rows and set the correct industry value for each row. However, since I have a huge number of rows, this is extremely expensive. So in order to award the bounty, I need an answer to two more questions:
- How can I avoid doing another pass to set the correct values?
- Why do I need to first load the rows and only then bind the datasource? This seems rather unintuitive and it causes my problem. What if the requirements ask for more data to be loaded at some point after the initial load? Would I then need to do the binding again?
The first thing you need to explain is why you have your column set to be a checkboxcolumn and not a comboboxcolumn? That may be your whole problem.
Now, by your code, you have a list already filled with your possible industries. How are you going to databind to any of your tuples(rows) if you don't have any rows to begin with? You should be databinding all of the cells in the column after you fill your dgv with your query result.
If that's not the issue I made a simple example of databinding a List<String>
to a cell.
Here's an example of putting a List into a datagridviewcombobox:
private List<String> theList = new List<String>();
public Form1()
{
InitializeComponent();
theList.Add("is");
theList.Add("this");
theList.Add("working");
theList.Add("yet?");
DataGridViewComboBoxCell dropDown = (DataGridViewComboBoxCell) dgv.Rows[0].Cells[0];
dropDown.DataSource = theList;
}
private void aButton_Click(object sender, EventArgs e)
{
theList.Clear();
theList.Add("I");
theList.Add("Knew");
theList.Add("this");
theList.Add("would");
theList.Add("work!!!");
}
How can I avoid doing another pass to set the correct values?
You can't (maybe). The datagridview is going to load one result at a time. Unless you are returning multiple values for that one cell, you are going to have to do another pass. The dgv is expecting to fill in the row with the data its given for that particular row. What you can try is not recognizing that column exists in your query (just remove it from the query) and that may cause it to pass over that column and not assign it a value. If that succeeds what you have to do next is make that cell automatically be those values when the row is created. You will know this is working if you were to just add rows and those cells were immediately populated even though there's no other data in any of the cells.
Why do I need to first load the rows and only then bind the datasource? This seems rather unintuitive and it causes my problem. What if the requirements ask for more data to be loaded at some point after the initial load? Would I then need to do the binding again?
You have to have an object to databind to. Databinding to the column won't make you databind to the cells in that column. It wouldn't even databind to the column header. You have to databind to the cells themselves. Once you databind, you can change that list as much as you want and the cells which are databinded to your list will change as well. Once you understand this concept databinding will be a lot easier to implement in the future.
Looking at your code again I suggest you create your columns before you fill your table with data. That way your dgv doesn't skip the creation of the column when you insert your results, but skips the data allotment for that column. In your foreach loop create a row, assign the data, databind the cell to your List<String>
and add the row to your datagridview. Once that's all working go grab yourself a drink and pat yourself on the back for a job well done.
Edit
Instead of doing what I previously said, try this. It doesn't even involve databinding a list. If you know what your string values are going to be for your drop down box at design time you can go to Columns>your_column>Items and enter the strings there. If you don't, that's ok too. Whenever you have your list of strings you can add them to the column's internal list like so:
foo DataGridViewComboBoxColumn = myDGV.columns("YourColumn")
foo.Items.Add("blarggg")
If you need to change the list at any time, all rows will be updated upon making said change.