RSS Feed
Download our iPhone app
Browse DevX
Sign up for e-mail newsletters from DevX


Add Flexible Sort Capabilities to ListView Controls : Page 3

Learn how to make the ListView control sort by a column when a user clicks on a column header, sort by all columns, or sort in just about any other way you can imagine.

Sorting By Complete Rows
Recall that the ListView control sorts only its items in the first column by default. To sort ListViewItems using all the control's columns, you use an approach similar to the one you use to sort by a single specific column—create a class that implements the IComparer interface and then set the control's ListViewItemSorter property to a new instance of the class.

This new row-comparer class is very similar to the one described previously. It also uses an ItemString method to build string representations of ListViewItems and then uses String.Compare to compare them. But there are two main differences between this class and the old one. The simpler difference is that the new class uses different code for its ItemString method so it can compare all of the columns rather than just the selected column. The new version of the function builds a single string containing all the item's values separated by null characters, making it easy to use String.Compare to compare two items' strings.

The String.Compare method returns —1 if the first string should sort before the second. The null character between fields has ASCII code 0 so it comes alphabetically before any other character. That means if two strings match at the beginning and one is shorter than the other, then the string containing the shorter field comes first alphabetically (because the null character will appear before whatever the other field contains next). If two fields match exactly, then later parts of the strings determine which item belongs first.

The second difference between this comparer class and the previous one is a little more complicated. The ListView control allows users to rearrange columns by clicking and dragging column headers to the right or left. This is one of the control's more useful features, so I didn't want to disable it. Unfortunately, allowing users to change column order makes sorting the items much more difficult.

For example, Figure 3 shows the example program in its original arrangement displaying the columns in the order: Author, Year, Pages, Title. The rows are sorted descending so Tom Holt is listed first because his name comes last alphabetically. His two books in the list were both published in 2006, so their order isn't determined until the Pages column. "Earth, Air, Fire, and Custard" has more pages than his other book so it is listed first in descending order.

Figure 3. Sorting Rows: Here the ListView control sorts all columns in descending order.
Figure 4. Columns Rearranged: Here the Year column has been dragged to the left of the Author column so the control sorts on the Year column first.
Figure 4 shows the same form after I dragged the Year column's header to the left of the Author column. Now my book "Expert One-on-One Visual Basic 2005 Design and Development" is listed first because it's the only book in the list that was published in 2007.

If you look down the list, you can see other effects of the row sorting method. For example, Sam Test's book with 1001 pages is listed before his other books because it has the most pages and the sort is in descending order. Similarly the book by Sam Test that has no Pages value is listed last because empty values come last in descending order.

To detect column reordering, the base classes' OnColumnReordered method would seem to be exactly what the doctor ordered—but it isn't, quite. While the method's name implies that it fires after column reordering has completed, the base class actually calls this method and fires the corresponding ColumnReordered event before the column is moved—so you don't yet know the new ordering of the control's columns. (The method and event should really be called OnColumnReordering and ColumnReordering, respectively. As long as we're dreaming, it would be nice to have OnColumnReordered and ColumnReordered. Or OnColumnReorderStart and OnColumnReorderEnd.)

While the column has not yet been moved in the OnColumnReordered method, the method does tell you which column will be moved and where it's going. That means with a little extra work you can figure out where all of the columns will be after the move is complete.

Listing 1 shows how the SortableListView control keeps track of where all of its columns will be after the user reorders columns.

In Listing 2, the m_SortSubitems array contains a list of the control's items in the order in which they should be used for sorting. For example, suppose the control's columns are Author, Year, Pages, and Title. These columns have indices 0, 1, 2, and 3. Now suppose the user rearranges the columns to Year, Author, Title, and Pages. Now m_SortSubitems contains the indices of these columns in the order in which they appear: 1, 0, 3, 2.

The control's SetSortSubitems method uses the control's current column ordering to initialize the m_SortSubitems array. It loops through the columns, saving each column's index in the m_SortSubitems entry corresponding to the column's DisplayIndex property. In the previous example, when the column reordering completes, the Author column (column 0) will be the second column (display position 1) so the code sets m_SortSubitems(1) = 0. Similarly the Year column (column 1) will be in display position 0 so the code sets m_SortSubitems(0) = 1.

The control overrides the base class's OnColumnReordered method to keep m_SortSubitems up to date. The code calls the base class's version of the method so it can perform its normal actions. Those actions include raising the ColumnReordered event so the main program can respond and possibly set e.Cancel to True to cancel the reordering. If e.Cancel is true, the OnColumnReordered method exits without taking further action.

If the event is not being canceled, the control calls SetSortSubitems to initialize the m_SortSubitems array. It then uses the MoveArrayItem method to move the index of the column that is being moved to its new position in the array and calls its Sort method to rearrange the items based on the new column order.

The MoveArrayItem method simply moves an integer from one position in an array to another. The code saves the value that is moving, uses Array.Copy to move the items in the array between the new and old positions, and then reinserts the moved value in its new position. MoveArrayItem is a handy method in its own right.

After the OnColumnReordered method updates the control's m_SortSubitems array, the comparer class's ItemString method can use that array to build comparison strings for items. The following code shows the new version of ItemString:

   ' Return a string representing this item as a
   ' null-separated list of the item sub-item values.
   Private Function ItemString(ByVal listview_item As ListViewItem) _
    As String
       Dim slvw As SortableListView = listview_item.ListView
       ' Make sure we have the sort sub-items' order.
       If slvw.m_SortSubitems Is Nothing Then slvw.SetSortSubitems()
       ' Make an array to hold the sort sub-items' values.
       Dim num_cols As Integer = slvw.Columns.Count
       Dim values(num_cols - 1) As String
       ' Build the list of fields in display order.
       For i As Integer = 0 To slvw.m_SortSubitems.Length - 1
           Dim idx As Integer = slvw.m_SortSubitems(i)
           ' Get this sub-item's value.
           Dim item_value As String = ""
           If idx < listview_item.SubItems.Count Then
               item_value = listview_item.SubItems(idx).Text
           End If
           ' Align appropriately.
           If slvw.Columns(idx).TextAlign = _
               HorizontalAlignment.Right _
               ' Pad so numeric values sort properly.
               values(i) = item_value.PadLeft(20)
               values(i) = item_value
           End If
       Next i
       ' Concatenate the values to build the result.
       Return String.Join(vbNullChar, values)
   End Function
The preceding code builds an array of strings to hold the item's column values, and initializes the entries to blank strings. The code loops through the entries in the m_SortSubitems array so it considers them in the order in which they currently appear on the control.

Next, the code gets each column's index. When the index is less than the number of objects in the ListViewItem.SubItems collection, then the sub-item exists so the code saves its value in the item_value variable. If the index is greater than or equal to the SubItems count, then this item does not have the corresponding sub-item (for example, there is no Title entry) so the code uses an empty string for the item's value.

If the column is right-aligned, the code pads the value on the left so numeric values sort properly.

After building the values for each field, the function uses String.Join to concatenate the results into a string with fields separated by null characters, and returns the result.

At this point, all the pieces for sorting are in place and the rest is automatic. If the user rearranges columns, the OnColumnReordered method rebuilds the m_SortSubitems array and calls the control's Sort method. Sort uses comparer objects to sort the items. The comparers use the ItemString function shown earlier, which uses the m_SortSubitems array to build the item strings in the correct column order. The result is a list of items sorted on each of the items' fields in their current display order.

More Sort Support
The SortableListView control demonstrates several useful techniques. It shows how to override base class methods (OnColumnClick and OnColumnReordered), move an item in an array (the MoveArrayItem method), and determine the current display order of columns in a ListView control.

More importantly, it shows how to use IComparer classes to implement custom sort orders. ListView controls are not the only objects that use comparer objects to support different sort orders. For example, you can use IComparer objects to sort items in TreeView or DataGridView controls. Various collection classes such as List, ArrayList, Hashtable, NameValueCollection, and SortedDictionary also support sorting with IComparer objects.

After you've mastered the SortableListView control, you'll find uses for IComparer objects all over the place. For example, you'll be able to use different objects to sort customers by name, customer ID, outstanding balance, or date overdue.

Rod Stephens is a consultant and author who has written more than a dozen books and two hundred magazine articles, mostly about Visual Basic. During his career he has worked on an eclectic assortment of applications for repair dispatch, fuel tax tracking, professional football training, wastewater treatment, geographic mapping, and ticket sales. His VB Helper web site receives more than 7 million hits per month and provides three newsletters and thousands of tips, tricks, and examples for Visual Basic programmers.
Email AuthorEmail Author
Close Icon
Thanks for your registration, follow us on our social networks to keep up-to-date