Reputation: 59
I have a DataGridView table that displays the properties of EmployeeModels. When compiling, this exception is thrown:
'ColumnCount property cannot be set on a data-bound DataGridView control.'
I have heard that setting .Datasource = null
before binding to the actual list can help, but doing so has not resolved the issue. I tried deleting the DataGridView and recreating it in case there was a bug, but this didn't resolve the issue.
//CREATE EMPLOYEE TABLE
public void CreateEmployeeTable()
{
EmployeeGridView.DataSource = null;
EmployeeGridView.DataSource = globalEmployeeList;
EmployeeGridView.AutoGenerateColumns = false;
EmployeeGridView.ColumnCount = 9;
EmployeeGridView.Columns[0].HeaderText = "Employee ID";
EmployeeGridView.Columns[0].DataPropertyName = "ID";
EmployeeGridView.Columns[1].HeaderText = "First Name";
EmployeeGridView.Columns[1].DataPropertyName = "FirstName";
EmployeeGridView.Columns[2].HeaderText = "Last Name";
EmployeeGridView.Columns[2].DataPropertyName = "LastName";
EmployeeGridView.Columns[3].HeaderText = "Nickname";
EmployeeGridView.Columns[3].DataPropertyName = "Nickname";
EmployeeGridView.Columns[4].HeaderText = "Hire Date";
EmployeeGridView.Columns[4].DataPropertyName = "HireDate";
}
private void EmployeeGridView_CellClick(object sender, DataGridViewCellEventArgs e)
{
DataGridViewCell selectedEmployeeCell = EmployeeGridView.CurrentCell;
int selectedEmployeeRow = selectedEmployeeCell.RowIndex;
int selectedEmployeeID = (int)EmployeeGridView.Rows[selectedEmployeeRow].Cells[0].Value;
jobTitleListBox.DataSource = globalEmployeeList.Where(person => person.ID == selectedEmployeeID).ToList();
}
Here is the SQL (And yes, I'm going to make this a stored procedure eventually; I'm just not there yet.)
public async Task<List<EmployeeModel>> GetEmployeeList()
{
using (IDbConnection connection = new System.Data.SqlClient.SqlConnection(GlobalConfig.CnnString("WorkDeskDB")))
{
var sql = @"SELECT e.id, e.FirstName, e.LastName, e.Nickname,
em.EmployeeID, em.Address, em.Type,
e.JobTitleID, jt.id, jt.Name,
p.EmployeeID, p.Number, p.Type,
ect.EmployeeID, ect.NameID, ect.InitialDate, ect.ExpirationDate,
ct.id, ct.Name
FROM dbo.Employees e
LEFT JOIN dbo.Emails em
ON em.EmployeeID = e.id
LEFT JOIN dbo.JobTitles jt
ON e.JobTitleID = jt.id
LEFT JOIN Phones p
ON p.EmployeeID = e.id
LEFT JOIN dbo.EmployeeCertificationType ect
ON ect.EmployeeID = e.id
LEFT JOIN dbo.CertificationType ct
ON ect.NameID = ct.id";
var employees = await connection.QueryAsync<EmployeeModel, EmailModel, TitleModel, PhoneModel, CertificationModel, EmployeeModel>(sql, (e, em, t, p, c) =>
{
e.EmailList.Add(em);
e.JobTitle = t;
e.PhoneList.Add(p);
e.CertificationList.Add(c);
return e;
}, splitOn: "EmployeeID, JobTitleID, EmployeeID, EmployeeID");
var result = employees.GroupBy(e => e.ID).Select(g =>
{
var groupedEmployee = g.First();
groupedEmployee.EmailList = g.Select(e => e.EmailList.Single()).ToList();
groupedEmployee.PhoneList = g.Select(e => e.PhoneList.Single()).ToList();
groupedEmployee.CertificationList = g.Select(e => e.CertificationList.Single()).ToList();
return groupedEmployee;
});
return result.ToList();
}
}
Upvotes: 0
Views: 181
Reputation: 30474
No, no, you should never ever fiddle with the cells that display your data.
Or in other words: separate your data from how it is displayed. Apart from that your code will be easier to read, it will also be easier to change. What, if for instance you want to display your data in a ListView, or maybe as a Graph.
Separating your data from how it is displayed also makes it easier to reuse it, to change the data and to test it.
So, assuming you know your SQL, the following should be no problem.
(I leave out all the async-await stuff, that is not part of your problem).
private IEnumerable<Employee> FetchEmployees()
{
// TODO: implement, fetch employees from the database
}
The nice thing is, that you hid the structure of your database. So if you decide to change the tables of your database, then the users of this method (= code, not operators), won't have to change. They don't even have to change if you decide to save the data in an XML file, or in a Dictionary. The latter might be quite handy if you need to unit test your code: you don't have to fill a database for every test, a dictionary with Employees will do.
So let's assume you have a method to fetch all employees in some smart way.
To display it, you could fiddle with the cells. But a much nice method would be to use the DataSource
of the DatagridView
Suppose your Employees have properties Id and Name, and several others. You want to show your Employees row by row, and you want to show properties Id and Name as columns:
DataGridViewColumn columnEmployeeId new DataGridViewColumn();
DataGridViewColumn columnEmployeeName new DataGridViewColumn();
columnEmployeeId.DataPropertyName = nameof(Employee.Id);
columnEmployeeName.DataPropertyName = nameof(Employee.Name);
Property DataPropertyName contains the name of the property that it should show.
Now all you have to do is Fetch the employees, and assign them to the DataSource of the DataGridView:
var employees = this.FetchEmployees().ToList();
this.DatagridView1.DataSource = employees;
This is enough display your employees. Changes that the operator makes to the displayed data are not updated in object employees. If you want that, you should put your data in an object that implements IBindingList
, like BindingList<T>
DataGridView1.DataSource = new BindingList<Employee>(FetchEmployees().ToList());
Now every change that the operator makes to the displayed Employees are automatically updated in the BindingList. Even if he adds or removes Employees.
The BindingList does not depend on how the data is displayed. So if you decide not to display column Id anymore, or if you decide to sort the displayed data by descending Name, this has no effect on the BindingList. If you look closely, you'll see that the BindingList in fact is not a List, but a Collection. There is no notion of Employee[3]
. This is expected, because if the operator sorts the displayed data, this Employee might not be shown in the Row[3] anymore.
Proper design of your form would contain methods like:
IEnumerable<Employee> FetchEmployees() {... see above}
BindingList<Employee> DisplayedEmployees
{
get => (BindingList<Employee>)this.dataGridView1.DataSource;
set => this.dataGridView1.DataSource = value;
}
Employee CurrentEmployee =>
(Employee) this.datagridView1.SelectedCell?.OwningRow.DataBoundItem;
IEnumerable<Employee> SelectedEmployees =>
this.DatagridView1.SelectedRows
.Select(row => row.DataBoundItem)
.Cast<Employee>();
Putting it all together:
On form load: display all employees:
void Form_Load(object sender, ...)
{
this.DisplayedEmployees = this.FetchEmployees();
}
After ButtonRemoveClicked: remove all Selected Employees:
void ButtonRemove_Clicked(object sender, ...)
{
void employeestoRemove = SelectedEmployees.ToList();
if (employeesToRemove.Any())
{
// TODO: ask operator if remove employees is ok
this.Repository.RemoveEmployees(employeesToRemove);
}
}
After an edit, all items in the datasource are updated:
void OnButtonOk_Clicked(object sender, ...)
{
var displayedEmployees = this.DisplayedEmployees;
var originalEmployees = this.FetchEmployees;
// TODO: find out which employees were added / removed / changed
// and Add / Remove / Update the data
}
Because you separated the handling of the database from the display of the employees the procedures are much smaller, easier to understand, easier to reuse, test, change.
Your code doesn't have to worry about sorting, column reordening, change in how data is displayed (is Birthday displayed as month-day-year, or as 2021-01-15?), your Form class does not have to worry about this, and does not need big changes if this is changed.
Upvotes: 2