Wednesday, May 6, 2009

Comparing your objects: IComparable & IComparer Interfaces (Part III)

Now, here is a useful example of IComparable and ICompare. Let's bind a List of our Business Objects to a DataGridView and Sort the list by column when a header column is double-clicked.

Create a WinForms application. Let's start with a form with a button and a DataGridView.
The button will create a List of Employees (see the previous post) and populate it. After that, we'll bind the list to our datagrid:

private void btnLoad_Click(object sender, EventArgs e)

{

List<Employee> employees = new List<Employee>();

//add employees to the list

employees.Add(new Employee("1", "Elijah", "Baley", 5000.50));

employees.Add(new Employee("2", "Daneel", "Olivaw", 3650.80));

employees.Add(new Employee("3", "Hari", "Seldon", 8450.25));

//bind the datagrid

dgvEmployees.DataSource = employees;

}



If you run the app and click the button you will get something like this:



Let's get to the Sort part. Remember we created some Comparer classes for our Employee Class. However, those comparers will work only to sort in an Ascending order. Let's create more to sort in Descending order. The only change here is that we will compare the objects backwards:

In ascending order we compare like this:
x.property.CompareTo(y.property)
In descending order we just use:
y.property.CompareTo(x.property)

So here you have all the comparers we will use:

//ASCENDING

public class EmployeeComparerFirstName : IComparer<Employee>

{

public int Compare(Employee x, Employee y)

{

return x.FirstName.CompareTo(y.FirstName);

}

}

public class EmployeeComparerLastName : IComparer<Employee>

{

public int Compare(Employee x, Employee y)

{

return x.LastName.CompareTo(y.LastName);

}

}

public class EmployeeComparerSalary : IComparer<Employee>

{

public int Compare(Employee x, Employee y)

{

return x.Salary.CompareTo(y.Salary);

}

}

//DESCENDING

public class EmployeeComparerEmployeeIdDSC : IComparer<Employee>

{

public int Compare(Employee x, Employee y)

{

return y.EmployeeId.CompareTo(x.EmployeeId);

}

}

public class EmployeeComparerFirstNameDSC : IComparer<Employee>

{

public int Compare(Employee x, Employee y)

{

return y.FirstName.CompareTo(x.FirstName);

}

}

public class EmployeeComparerLastNameDSC : IComparer<Employee>

{

public int Compare(Employee x, Employee y)

{

return y.LastName.CompareTo(x.LastName);

}

}

public class EmployeeComparerSalaryDSC : IComparer<Employee>

{

public int Compare(Employee x, Employee y)

{

return y.Salary.CompareTo(x.Salary);

}

}



You may wonder why we have a desceding comparer for EmployeeId property but not the ascending one. That's because the ascending comparer for that property is the default comparer in our Employee Class when it implements IComparable. That's in the CompareTo method (see part I).

We can use any of this comparers to sort the columns of our datagrid, the problem is how to know which comparer should we use. To decide that we'll create a method that will return the right IComparer we need, based on the name of the property that column is bound to and the sort order.

private IComparer<Employee> getEmployeeComparer(String propertyName, System.Windows.Forms.SortOrder sortDirection)

{

switch (propertyName)

{

case "FirstName":

if (sortDirection == SortOrder.Ascending)

return new EmployeeComparerFirstName();

else

return new EmployeeComparerFirstNameDSC();

case "LastName":

if (sortDirection == SortOrder.Ascending)

return new EmployeeComparerLastName();

else

return new EmployeeComparerLastNameDSC();

case "Salary":

if (sortDirection == SortOrder.Ascending)

return new EmployeeComparerSalary();

else

return new EmployeeComparerSalaryDSC();

default:

if (sortDirection == SortOrder.Ascending)

return null;

else

return new EmployeeComparerEmployeeIdDSC();

}

}


You can see that if the property name is not found, the list will be sorted by EmployeeId. Additionally, if the sort order is ascending the method will return null. That's because if the Sort method of List receives no comparer at all, it'll use the default comparer of the object: the CompareTo method.

You can write this method in the Employee class making it static or in a different class with all the other comparers. For now let's just put it in the code of our Form.

The next step is to handle the event ColumnHeaderMouseDoubleClick of the DataGridView, so when the user double-clicks the header of a column we will sort that column.

private void dgvEmployees_ColumnHeaderMouseDoubleClick(object sender, DataGridViewCellMouseEventArgs e)

{

//get list from datasource

List<Employee> employees = (List<Employee>)dgvEmployees.DataSource;

//set sort order

SortOrder sortOrder = (dgvEmployees.Columns[e.ColumnIndex].HeaderCell.SortGlyphDirection == SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending);

//sort list

employees.Sort(getEmployeeComparer(dgvEmployees.Columns[e.ColumnIndex].DataPropertyName, sortOrder));

//remove sort glyph from any other column

unmarkSortedColumns(dgvEmployees);

//set sort glyph for current column

dgvEmployees.Columns[e.ColumnIndex].HeaderCell.SortGlyphDirection = sortOrder;

//refresh grid to see the changes

dgvEmployees.Refresh();

}


Here is the big thing.
1) We cast the data source of the DataGridView to an Employee List.

List<Employee> employees = (List<Employee>)dgvEmployees.DataSource;


2) We decide the new sort order. If the column is already sorted with ascending order then we will sorted descending, else if the sort order is descending or the column is not sorted at all we will use ascending order.

SortOrder sortOrder = (dgvEmployees.Columns[e.ColumnIndex].HeaderCell.SortGlyphDirection == SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending);


3) Now we sort the List (notice that we sort the List and not the DataGridView) using our GetEmployeeComparer method to get the right comparer we need. To do this we pass the sort order and the name of the property. To get the name of the property just look at the DataPropertyName attribute of the current column.

employees.Sort(getEmployeeComparer(dgvEmployees.Columns[e.ColumnIndex].DataPropertyName, sortOrder));


4) An additional step is to remove the sorted glyph to any other column that was previously sorted. To do this we write the function unmarkSortedColumns passsing the our DataGridView, this way we can reuse this method when sorting other grids.

private void unmarkSortedColumns(DataGridView dataGrid)

{

foreach (DataGridViewColumn col in dataGrid.Columns)

{

if (col.HeaderCell.SortGlyphDirection != SortOrder.None)

col.HeaderCell.SortGlyphDirection = SortOrder.None;

}

}


5) After that we set the sort glyph to the current column we are sorting. Remember that the SortGlyphDirection property or the HeaderCell of our column is the one that sets that arrow icon in the column header that indicates the column is sorted.

dgvEmployees.Columns[e.ColumnIndex].HeaderCell.SortGlyphDirection = sortOrder;


6) And the last thing, but not less important, is to refresh the DataGridView. Otherwise, how would it know that the List has been order?

dgvEmployees.Refresh();


And that's it for now, your DataGrid will be sorted by the column you double-click:



In conclusion: you can implement IComparable and IComparer to compare you business objects by some property or based on an algorithm. A practical usage of these interfaces is to sort a list of business objects and sort the columns of a DataGridView when it is bound to a list of business objects.

That's it for this serie of posts, however expect a surprise about topic. soon.

See Part I and Part II

No comments:

Post a Comment