Introduction
Recently, I had to face the exciting challenge of sorting a ListView
. So I searched CodeProject and other sites, but what I found was mostly too sophisticated or complex for my simple mind. I did not want to write inherited controls or loads of lines of code in the Form - just to sort a ListView
!
The ListView
itself provides a sort function if an IComparer
implementation is assigned to the property ListViewItemSorter
. So there must be a simple way to use this function. Thus, I tried to find a design for an extendable, easy-to-use ListViewSorter by myself and, eventually, the article Extended ListView by Michiel van Eerd at CodeProject kicked off my inspiration.
Next, you will find a bunch of explanations and diagrams. If you just want to know how to use the code, jump to the Using the code-section - it's shorter. Finally, if you want to know why it works - come back here.
Warranty
This article and the provided code do not claim to be complete or perfect! Rather it is an attempt to solve the given problem.
Design
Until I found the mentioned article, I was not really aware of the functions Decimal.TryParse(String, Decimal)
or DateTime.TryParse(String, DateTime)
. These functions allow you to try (!) to convert nearly everything into Decimal
or DateTime
without headaches. Even these methods are a bit dirty, they work sufficient for sorting purposes (in my opinion).
With a knowledge about these methods, I had the idea to write a group of ISortComparer
s which can be assigned to certain columns of a ListView
at runtime. The ISortComparer
interface extends the IComparer
only by the property SortOrder
.
The intention of the design was not to couple the comparer directly to the ListView
, finally they shall be usable for all kinds of collections. So they stand independent from the System.Windows.Forms
namespace, and an adapter mechanism is needed.
To make life easier, the ISortComparer
implementations can inherit from the abstract class SortComparerBase
which provides the common functionalities like holding the SortOrder
property or the null
-value handling. The abstract method Compare
has to be implemented by the concrete implementations.
As an example, a short commendation of the DateComparer
:
public class DateComparer : SortComparerBase{
public override int Compare(object x, object y)
{
if (base.sortorder == SortOrder.None)
return 0;
if (base.HasEmptyValue(x, y))
return base.CompareEmpty(x, y);
DateTime x1 = DateTime.MinValue;
DateTime y1 = DateTime.MinValue;
if (DateTime.TryParse(x.ToString(), out x1) &&
DateTime.TryParse(y.ToString(), out y1))
{
if (base.sortorder == SortOrder.Ascending)
return DateTime.Compare(x1, y1);
else
return DateTime.Compare(y1, x1);
}
else
{
return 0;
}
}
}
Comments to the code above:
- If there is nothing to sort, everything is equal.
- Handle the case if at least one of the values is null or empty (these methods are implemented in the
SortComparerBase
).
- The
DateTime.TryParse
method will write its (successful) results into this variables. In case of failure, the current values of this variable will not be overwritten. So set them to values which imply a position after sorting.
- If a
TryParse
method fails (e.g., because the value cannot be converted into the required Type) it returns false
. If this happens, 0 (equal) will be returned.
This adapter to connect the ISortComparer
s to the ListView
was realized by the class ListViewSorter
which holds a reference to the ListView
and handles the ListView
's ColumnClick
event. It also holds a Dictionary with ISortComparer
s for each column. As mentioned, the ISortComparer
s do not know a class called ListView
or its items. Hence, a mediator or proxy was implemented which connects the ListView
with specific ISortComparer
implementations: the ListViewItemComparer
. This class holds the to-be sorted column as well as the specified ISortComparer
for this column. Since it implements the IComparer
interface, it can be set as the ListView
's ListViewItemSorter
property.
When the sort routine of the ListViewSorter
is called by the TreeView
's ColumnClick
event, a new ListViewItemComparer
instance is created, and its Comparer
property is set by the columns comparer-dictionary entry. Then the ListViewItemComparer
is set as the ListView
's ListViewItemSorter
property, and ListView.Sort()
is called. That's it.
Using the code
After many words in theory, now less words in practice. Actually, only two lines of code are required to sort a ListView
using arbitrary columns.
public partial class Form1 : Form
{
ListViewSorter listviewsorter = new ListViewSorter();
private void Form1_Load(object sender, EventArgs e)
{
listviewsorter.ListView = this.listView1;
listviewsorter.ColumnComparerCollection["Int"] = new NumericComparer();
listviewsorter.ColumnComparerCollection["Decimal"] = new NumericComparer();
listviewsorter.ColumnComparerCollection["Date"] = new DateComparer();
}
}
Note: In the above example, the comparer for the String
-column is left out because the StringComparer
is the default comparer.
If you want to display specific images on the column headers indicating the sort column and the order, the ListViewSorter
provides the properties ImageKeyUp
and ImageKeyDown
to define the keys of the images of the ListView
's SmallImageList
property.
Extensibility example
If you are in need for a specific comparer, it should be no problem to write your own ISortComparer
implementation and extend the functionality of the ListViewSorter
. In the demo application, I implemented a GermanPostalCodeCityByCityComparer
and a GermanPostalCodeCityByPostalCodeComparer
(couldn't find shorter names) as examples.
In Germany, each area has a unique five digit postal code. It is common usage to display an address' city part as a combination of the postal code and the city's name, separated by a space (e.g., "22307 Hamburg"). Three meaningful scenarios to sort this combinations could be:
- alphanumeric by the complete token
- alphanumeric by the city's name
- numeric by the postal code
The GermanPostalCodeCityByCityComparer
matches b. and sorts the items by the city's name ("Hamburg", "Berlin",...). The GermanPostalCodeCityByPostalCodeComparer
matches c. and sorts the items numeric by the postal code ("01465", "22307", ...). For option c., the default StringComparer
can be used.
I guess these comparers are not absolutely necessary for most of you, but they can give an idea of what ISortComparer
s can be useful for. If you read the code, you will see how easy it is to enhance the sorting capabilities.
Challenge: Not implemented are comparers which sort first by name and then by postal code or vice versa. Or comparers which can sort various columns in a certain order...
Points of Interest
SortOrder
: There is an enumeration with the same name in the System.Windows.Forms
namespace. Since the use of something like a sort-order should not force the import of that namespace, it became necessary to include a SortOrder
enumeration in a more common namespace. Like Microsoft, it is my opinion that SortOrder
describes its meaning perfectly, and I refrained from naming it SortOrderX
.
Namespaces: As mentioned somewhere above, it was not my intention to couple the comparers directly to the ListView
and System.Windows.Forms
namespaces. Thus, I separated them and the ListViewSorter
in their own namespaces.
A look ahead
The mechanism described in this article should be sufficient to sort Collection
s as well. Provided the necessary time, I will update this article or write a separate one regarding this subject.
History
- 2006-10-12 - First published.