Training



Generic, Reusable Multi-Property Sorter Level 300
DevForce Express
November 28, 06

.NET provides the System.Component.BindingList(Of T) to house objects for databinding. DevForce provides the IdeaBlade.Util.BindableList(Of T), which improves upon the BindingList in three important areas:

(1) Support for nested binding and dynamic properties.

(2) Support for ListManagers that can watch external sources for objects that should be automatically added       to a list.

(3) Enhanced sorting facilities.

In this Tech Tip we’ll look at the BindableList’s sorting facilities and see how to write a generic, reusable IComparer that permits ascending or descending multi-level sorts on an unlimited number of properties.

Sorting a BindableList(Of T)

System.Component.BindingList provides a method, ApplySortCore(), which must be overridden by a derived class to provide sort capability on the list. BindableList(Of T) does override this method and also provides five overloads of an ApplySort method that uses it. You can call ApplySort() with a string-valued property name, a PropertyDescriptor, or a System.Collections.Generic.IComparer(Of T). Three of the overloads support a pKeepListSorted parameter. By setting this to true, your list stays sorted even as new items are added and existing items are changed. 

The most powerful of the ApplySort() overloads requires a System.Collections.Generic.IComparer(Of T) that provides the logic for determining which of two items should appear before the other one in a list. The signature for that overload is as follows:

 

C#:

public void ApplySort(System.Collections.Generic.IComparer<T> pComparer,

  bool pKeepListSorted)

{

VB.NET:

Public Sub ApplySort( _

  ByVal pComparer As System.Collections.Generic.IComparer(Of T), _

  ByVal pKeepListSorted As Boolean)

 

Here’s a simple implemention of the IComparer that does a hard-coded ascending sort of Employees on LastName, FirstName:

 

C#:

public class EmployeeLastNameFirstNameAscendingComparer : IComparer<Employee> { 

  public int Compare(Model.Employee x, Model.Employee y) { 

       int retVal = 0; 

       if (x.IsNullEntity & y.IsNullEntity)     {

         //Both are null; don't change their order

         retVal = 0;

       }

       else if (x.IsNullEntity & ! y.IsNullEntity)     {

         //x is null and y is not; make x first

         retVal = -1;

       }

       else if (! x.IsNullEntity & y.IsNullEntity)     {

         //y is null and x is not; make y first

         retVal = 1;

       }

       else   {

         //Neither is null; do the comparison.

         //Start by comparing the LastNames.

         retVal = string.Compare(x.LastName, y.LastName, true, System.Globalization.CultureInfo.CurrentCulture);

         //Note that we’ve used an overload of String.Compare that

         //takes a CurrentCulture parameter. This will ensure that

         //our sort works even our app is moved to a different

         //country and language, where the rules for sorting may

         //be different.

         if (retVal == 0)     {

              //Same LastName; let the FirstName determine the ordering.

              retVal = string.Compare(x.FirstName, y.FirstName, true, System.Globalization.CultureInfo.CurrentCulture);

         }

       }

       return retVal;

  }

}


VB.NET:

Public Class EmployeeLastNameFirstNameAscendingComparer

  Implements IComparer(Of Employee)

 

  Public Function Compare(ByVal x As Model.Employee, ByVal y As Model.Employee) As Integer _

      Implements System.Collections.Generic.IComparer(Of Model.Employee).Compare

 

    Dim retVal As Integer = 0

 

    If x.IsNullEntity And y.IsNullEntity Then

      'Both are null; don't change their order

      retVal = 0

    ElseIf x.IsNullEntity And Not y.IsNullEntity Then

      'x is null and y is not; make x first

      retVal = -1

    ElseIf Not x.IsNullEntity And y.IsNullEntity Then

      'y is null and x is not; make y first

      retVal = 1

    Else

      'Neither is null; do the comparison.

      'Start by comparing the LastNames.

      retVal = String.Compare(x.LastName, y.LastName, True, _

        System.Globalization.CultureInfo.CurrentCulture)

      'Note that we’ve used an overload of String.Compare that

      'takes a CurrentCulture parameter. This will ensure that

      'our sort works even our app is moved to a different

      'country and language, where the rules for sorting may

      'be different.

      If retVal = 0 Then

        'Same LastName; let the FirstName determine the ordering.

        retVal = String.Compare(x.FirstName, y.FirstName, True, _

          System.Globalization.CultureInfo.CurrentCulture)

      End If

    End If

    Return retVal

  End Function

End Class

 

We could use the above IComparer to sort a BindableList of Employee objects in a statement like the following:

 

C#:

mEmployees.ApplySort(new LastNameFirstNameComparer(), true);

VB.NET:

mEmployees.ApplySort(New LastNameFirstNameComparer(), True)

 

Fair enough, but perhaps you see a problem: We could end up writing a lot of such IComparers and would have to anticipate every desired sort. So let’s cut to the chase and look at a very general IComparer. This one works on any business object (Entity) type; will sort on any number of properties; and allows for specifying the sort direction desired on each included property. Now that’s an IComparer you can really ride into town on!

 

C#:

using IdeaBlade.Persistence;

using IdeaBlade.Util;

/// <summary>
/// Arbitrary n-column sort
/// </summary>
/// <remarks>This version works with all properties: simple, calculated, nested.</remarks>

public class MultiPropertyComparer<T> where T: Entity : IComparer<T> {

 

  public MultiPropertyComparer(BindableList<SortProperty<T>> pSortProperties)  {

       mSortProperties = pSortProperties;

  }

 

  public int Compare(T x, T y)  {

 

       int retVal = 0;

 

       if (x.IsNullEntity & y.IsNullEntity)     {

         //Both are null; don't change their order

         retVal = 0;

       }

       else if (x.IsNullEntity & ! y.IsNullEntity)     {

         //x is null and y is not; make x first

         retVal = -1;

       }

       else if (! x.IsNullEntity & y.IsNullEntity)     {

         //y is null and x is not; make y first

         retVal = 1;

       }

       else   {

         //Neither is null; do the comparison

         foreach (SortProperty<T> aSortProperty in mSortProperties) {

              IdeaBlade.Util.PropertyComparer<T> comparer = new IdeaBlade.Util.PropertyComparer<T>(aSortProperty.Descriptor, aSortProperty.Direction);

              retVal = comparer.Compare(x, y);

              if (retVal != 0) {

                break;

              }

         }

       }

       return retVal;

  }

 

#region Private Fields

  private BindableList<SortProperty<T>> mSortProperties;

#endregion

 

}


VB.NET:

Imports IdeaBlade.Persistence

Imports IdeaBlade.Util 

''' <summary>
''' Arbitrary n-column sort
''' </summary>
''' <remarks>This version works with all properties: simple, calculated, nested.</remarks>

Public Class MultiPropertyComparer(Of T As Entity)

  Implements IComparer(Of T)

 

  Public Sub New(ByVal pSortProperties As BindableList(Of SortProperty(Of T)))

    mSortProperties = pSortProperties

  End Sub

 

  Public Function Compare(ByVal x As T, ByVal y As T) As Integer _

      Implements System.Collections.Generic.IComparer(Of T).Compare

 

    Dim retVal As Integer

 

    If x.IsNullEntity And y.IsNullEntity Then

      'Both are null; don't change their order

      retVal = 0

    ElseIf x.IsNullEntity And Not y.IsNullEntity Then

      'x is null and y is not; make x first

      retVal = -1

    ElseIf Not x.IsNullEntity And y.IsNullEntity Then

      'y is null and x is not; make y first

      retVal = 1

    Else

      'Neither is null; do the comparison

      For Each aSortProperty As SortProperty(Of T) In mSortProperties

        Dim comparer As New IdeaBlade.Util.PropertyComparer(Of T) _

          (aSortProperty.Descriptor, aSortProperty.Direction)

        retVal = comparer.Compare(x, y)

        If retVal <> 0 Then

          Exit For

        End If

      Next

    End If

    Return retVal

  End Function

 

#Region "Private Fields"

  Private mSortProperties As BindableList(Of SortProperty(Of T))

#End Region

 

End Class

 
The SortProperty(Of T) class used by the above IComparer is shown below:
 

C#:

using System.ComponentModel;

using IdeaBlade.Persistence;

using IdeaBlade.Util.PropertyDescriptorFns;

 

public class SortProperty<t> where t: entity {

  public SortProperty(string pPropertyPath, ListSortDirection pDirection)  {

       mPropertyPath = pPropertyPath;

       mDirection = pDirection;

       mDescriptor = GetPropertyDescriptor(typeof(t), pPropertyPath);

  }

 

  public SortProperty(PropertyDescriptor pDescriptor,

    ListSortDirection pDirection) {

       mDescriptor = pDescriptor;

       mDirection = pDirection;

  }

 

  public PropertyDescriptor Descriptor  {

       get    {

         return mDescriptor;

       }

       set    {

         mDescriptor = value;

       }

  }

 

  public ListSortDirection Direction  {

       get    {

         return mDirection;

       }

       set    {

         mDirection = value;

       }

  }

 

#region Private Fields

  private string mPropertyPath;

  private PropertyDescriptor mDescriptor;

  private ListSortDirection mDirection;

#endregion

 

}

 

VB.NET: 

Imports System.ComponentModel

Imports IdeaBlade.Persistence

Imports IdeaBlade.Util.PropertyDescriptorFns

 

Public Class SortProperty(Of t As entity)

  Public Sub New(ByVal pPropertyPath As String, ByVal pDirection As ListSortDirection)

    mPropertyPath = pPropertyPath

    mDirection = pDirection

    mDescriptor = GetPropertyDescriptor(GetType(t), pPropertyPath)

  End Sub

 

  Public Sub New(ByVal pDescriptor As PropertyDescriptor, ByVal pDirection As ListSortDirection)

    mDescriptor = pDescriptor

    mDirection = pDirection

  End Sub

 

  Public Property Descriptor() As PropertyDescriptor

    Get

      Return mDescriptor

    End Get

    Set(ByVal value As PropertyDescriptor)

      mDescriptor = value

    End Set

  End Property

 

  Public Property Direction() As ListSortDirection

    Get

      Return mDirection

    End Get

    Set(ByVal value As ListSortDirection)

      mDirection = value

    End Set

  End Property

 

#Region "Private Fields"

  Private mPropertyPath As String

  Private mDescriptor As PropertyDescriptor

  Private mDirection As ListSortDirection

#End Region

 

End Class

 

Here are a few sample usages of the MultiPropertyComparer

1. Sort Employees ascendingly by LastName, FirstName, passing the sort properties as string-valued names:

 

C#:

BindableList<SortProperty<Employee>> sortProperties =

  new BindableList<SortProperty<Employee>()>();

sortProperties.Add(new SortProperty<Employee>("LastName",

  ListSortDirection.Ascending));

sortProperties.Add(new SortProperty<Employee>("FirstName",

  ListSortDirection.Ascending));

mEmployees.ApplySort(new MultiPropertyComparer<Employee>(sortProperties), true);

 

VB.NET:

Dim sortProperties As New BindableList(Of SortProperty(Of Employee))

sortProperties.Add(New SortProperty(Of Employee)("LastName", _

   ListSortDirection.Ascending))

sortProperties.Add(New SortProperty(Of Employee)("FirstName", _

   ListSortDirection.Ascending))

mEmployees.ApplySort(New MultiPropertyComparer(Of Employee)(sortProperties), True)

 

2. Sort Employees ascendingly by LastName, FirstName, passing the sort properties as PropertyDescriptors:

 

C#:

BindableList<SortProperty<Employee>> sortProperties =

  new BindableList<SortProperty<Employee>()>();

sortProperties.Add(new SortProperty<Employee>(EntityPropertyDescriptors.Employee.LastName,

  ListSortDirection.Ascending));

sortProperties.Add(new SortProperty<Employee>(EntityPropertyDescriptors.Employee.FirstName,

  ListSortDirection.Ascending));

mEmployees.ApplySort(new MultiPropertyComparer<Employee>(sortProperties), true);

VB.NET:

Dim sortProperties As New BindableList(Of SortProperty(Of Employee))

sortProperties.Add(New SortProperty(Of Employee)( _

  EntityPropertyDescriptors.Employee.LastName, ListSortDirection.Ascending))

sortProperties.Add(New SortProperty(Of Employee)( _

  EntityPropertyDescriptors.Employee.FirstName, ListSortDirection.Ascending))

mEmployees.ApplySort(New MultiPropertyComparer(Of Employee)(sortProperties), True)

 

3. Sort Employees descendingly by the count of Orders on which they acted as SalesRep; then ascendingly by their TotalOrderRevenue: (*)

 

C#:

BindableList<SortProperty<Employee>> sortProperties =

  new BindableList<SortProperty<Employee>()>();

sortProperties.Add(new SortProperty<Employee>(PropertyDescriptorFns.GetPropertyDescriptor(

  typeof(Employee), "Orders.Count"), ListSortDirection.Descending));

sortProperties.Add(new SortProperty<Employee>(PropertyDescriptorFns.GetPropertyDescriptor(

  typeof(Employee), "TotalOrderRevenue"), ListSortDirection.Ascending));

mEmployees.ApplySort(new MultiPropertyComparer<Employee>(sortProperties), true);

 

VB.NET:

Dim sortProperties As New BindableList(Of SortProperty(Of Employee))

sortProperties.Add(New SortProperty(Of Employee) _

  (PropertyDescriptorFns.GetPropertyDescriptor( _

  GetType(Employee), "Orders.Count"), ListSortDirection.Descending))

sortProperties.Add(New SortProperty(Of Employee) _

  (PropertyDescriptorFns.GetPropertyDescriptor( _

  GetType(Employee), "TotalOrderRevenue"), ListSortDirection.Ascending))

mEmployees.ApplySort(New MultiPropertyComparer(Of Employee)(sortProperties), True)

 

(*) We have used the GetPropertyDescriptor() method in this case to get the necessary PropertyDescriptors rather than looking for them in the EntityPropertyDescriptors collection, because neither Orders.Count nor TotalOrderRevenue can be found in the latter. TotalOrderRevenue isn’t there because it’s a custom property, and the Object Mapper, keeping the clean separation between the developer- and DataRow-level classes, only generates EntityPropertyDescriptors for properties that are defined in the latter. The developer can manually add EntityPropertyDescriptors to the developer-level class, but the above code sample does not assume this has been done.

The case of Orders.Count is different. Like TotalOrderRevenue, it has no EntityPropertyDescriptor in the EmployeeDataRow class; nor is there one in the Order class, since Count is not a property of Order, but of the ICollection (list) in which the Employee’s Orders reside. GetPropertyDescriptor() buffers both kinds of issue, returning a PropertyDescriptor that will do the desired job.

All Tech Tips