Sorting MultiColumn ListView


I've read Nipun Tomar's "Sort a Multicolumn ListView in C#" article today and while it was a great article, I feel there is some room for improvement. Mainly some things can be done clearer and more efficiently.

If you've read the article, you know the sorting can be easily done by inheriting IComparer interface.

First, let's add some items to the ListView. You can add the following code to the form's constructor:

this.listView.Items.AddRange(new ListViewItem[] {
    new ListViewItem(new string[] {
        "Green River",
        "1984",
        "15/06/1988"}),
    new ListViewItem(new string[] {
        "Tad",
        "1988",
        "01/03/1989"}),
    new ListViewItem(new string[] {
        "Alice In Chains",
        "1987",
        "21/08/1990"}),
    new ListViewItem(new string[] {
        "Kyuss",
        "1988",
        "19/04/1990"})
});

The built-in enumeration SortOrder is a bit difficult the work with, because the order of items is:

0. None
1. Ascending
2. Descending

We have to multiply IComparer.Compare's return value based on sorting order, which means that the following order of enum items, with interval [-1, 1] instead of [0, 2], is easier to work with:

-1.) Descending (multiply by -1)
0.) None (multiply by 0)
1.) Ascending (multiply by 1)

Here is our enumeration and its variable with initial value "None":

public enum MySortOrder { Descending = -1, None, Ascending };
private MySortOrder order = MySortOrder.None;

We have to set only one item to a specific value (-1 in this case), the rest will follow automatically.

Of course it's not hard to get proper values from the build-in SortOrder with a mathematical formula, but why add necessary code if we can create our own enumeration.

Let's add a field that keeps track of last clicked column (initial value is -1 because no item has been clicked yet):

private int curr_column = -1;

Okay, now let's move to creating IComparer-inherited class called "ItemComparer". First we'll add two fields, one is for column's index, the other is for the type of sorting.

private int _index;
private int _order;

The constructor:

public ItemComparer(int index, int order)
{
    _index = index;
    _order = order;
}

I'll move step by step through Compare method:

public int Compare(object A, object B)

The method should check whether ordering is set to None (multiply by 0) at the very beginning, because if it is, the return value will always be 0, so the rest of the method is a waste of system resources. Same goes for comparing an object to itself - remember that objects are compared by reference and not by value.

if (_order == 0 || A == B) return 0;

The previous condition being false doesn't only mean that A and B are two separate objects, it also means that at least one of the two objects isn't null.

So the following two statements is all we need to handle null objects properly:

else if (A == null) return -1;
else if (B == null) return 1;

Now that we checked the most necessary things, we can create two objects (references) for each object as a ListViewItem:

ListViewItem itemA = A as ListViewItem;
ListViewItem itemB = B as ListViewItem;

And now we can truly check if the two column items have equal Text property:

if (itemA.SubItems[_index].Text == itemB.SubItems[_index].Text)
    return 0;

Let's start comparing items as different data-types. First, we'll check if the text we're comparing is a date. If it is, we can return from the function without executing the rest of the code:

DateTime dateA, dateB;
if (DateTime.TryParse(itemA.SubItems[_index].Text, out dateA) &&
    DateTime.TryParse(itemB.SubItems[_index].Text, out dateB))
{
    return _order * DateTime.Compare(dateA, dateB);
}

Note that we have to multiply DateTime.Compare's return value by _order, which is either 1 or -1.

If it isn't a date, then maybe it's a number:

double doubleA, doubleB;
if (double.TryParse(itemA.SubItems[_index].Text, out doubleA) &&
    double.TryParse(itemB.SubItems[_index].Text, out doubleB))
{
    return _order * (doubleA > doubleB ? 1 : (doubleA < doubleB ? -1 : 0));
}

And if isn't a number, then we'll just compare the text as string-type:

return _order * String.Compare(itemA.SubItems[_index].Text,
    itemB.SubItems[_index].Text);

Now that we're finished with  ItemComparer, we can move to adding an event handler to our ListView's ColumnClick event. Since we will use the handler only for this particular event, we can write an anonymous method.

First we'll check if the clicked column is the same as the last clicked column. If it isn't, we'll set the order of sorting to Ascending:

this.listView.ColumnClick += (object sender, ColumnClickEventArgs e) =>
{
    if (curr_column != e.Column) order = MySortOrder.Ascending;

And if columns are the same, we'll check the incremented order if it's outside of the [-1, 1] interval, so that the sorting order cycles from Ascending (1) to Descending (-1) to None (0) and back to Ascending (1):

else order = (int)++order > 1 ? MySortOrder.Descending : order;

Now all we have to do is set the sorter by creating new ItemComparer:

this.listView.ListViewItemSorter = new ItemComparer(e.Column, (int)order);

And last, assign column's index to curr_column and finish the anonymous method with a curly brace and a semicolon:

    curr_column = e.Column;
};

Whole listing:

using System;
using System.Collections;
using System.Windows.Forms;
namespace ListViewSortColumn
{
    public partial class MainForm : Form
    {
        public enum MySortOrder { Descending = -1, None, Ascending };
        private MySortOrder order = MySortOrder.None;
        private int curr_column = -1;
        public MainForm()
        {
            InitializeComponent();
            this.listView.Items.AddRange(new ListViewItem[] {
                new ListViewItem(new string[] {
                    "Green River",
                    "1984",
                    "15/06/1988"}),
                new ListViewItem(new string[] {
                    "Tad",
                    "1988",
                    "01/03/1989"}),
                new ListViewItem(new string[] {
                    "Alice In Chains",
                    "1987",
                    "21/08/1990"}),
                new ListViewItem(new string[] {
                    "Kyuss",
                    "1988",
                    "19/04/1990"})
            });
            this.listView.ColumnClick += (object sender, ColumnClickEventArgs e) =>
            {
                if (curr_column != e.Column) order = MySortOrder.Ascending;
                else order = (int)++order > 1 ? MySortOrder.Descending : order;
                this.listView.ListViewItemSorter = new ItemComparer(e.Column, (int)order);
                curr_column = e.Column;
            };
        }
    }
    class ItemComparer : IComparer
    {
        private int _index;
        private int _order;
        public ItemComparer(int index, int order)
        {
            _index = index;
            _order = order;
        }
        public int Compare(object A, object B)
        {
            if (_order == 0 || A == B) return 0;
            else if (A == null) return -1;
            else if (B == null) return 1;
            ListViewItem itemA = A as ListViewItem;
            ListViewItem itemB = B as ListViewItem;
            if (itemA.SubItems[_index].Text == itemB.SubItems[_index].Text)
                return 0;
            DateTime dateA, dateB;
            if (DateTime.TryParse(itemA.SubItems[_index].Text, out dateA) &&
                DateTime.TryParse(itemB.SubItems[_index].Text, out dateB))
            {
                return _order * DateTime.Compare(dateA, dateB);
            }
            double doubleA, doubleB;
            if (double.TryParse(itemA.SubItems[_index].Text, out doubleA) &&
                double.TryParse(itemB.SubItems[_index].Text, out doubleB))
            {
                return _order * doubleA > doubleB ? 1 : (doubleA < doubleB ? -1 : 0);
            }
            return _order * String.Compare(itemA.SubItems[_index].Text,
                itemB.SubItems[_index].Text);
        }
    }
}

1.gif

This is it. If you have any suggestions or ideas how to make the above code more efficient and clear, please post a comment or send me a message. I come here to learn and I'll be thankful for any input.

Thanks for reading, code safely!