Wednesday, November 02, 2005

Making a Sortable Collection

Today I needed a collection that would be self-sorting, and I came up with this handy technique for making one. This approach should work for any object that inherits the System.Collections.CollectionBase object.



The key to this approach is that part of the underlying plumbing of the CollectionBase is an ArrayList. Since ArrayLists are self-sorting, wiring this up is a breeze.



The sort method of an ArrayList have a few overloads. One takes no parameters, and the other takes an IComparer object. What does it mean? Well, if you don't specify a comparer object, the array list will be looking to the objects in the collection to perform comparisons against one another as part of the sort aglorithm.



Consider the following example (I like using the good old dog object). Let's say you have a simple "Dog" class that has two properties: Name, and BirthDate. You want to write a collection to store Dog objects, and you need the ability to quickly sort the Dogs in this collection by either name or Birthdate. (Maybe you're writing some kennel software or something - who knows?)



First, a look at the Dog object. It's pretty standard, but you'll notice that it implements IComparer (see my "Implementation and Inheritence Primer" post for more information about this).




public class Dog : IComparable
{
private string _name = string.Empty;
private DateTime _birthDate = new DateTime();

public Dog()
{
//Empty Constructor
}

public Dog(string itemName, DateTime birthDate)
{
_name = itemName;
_birthDate = birthDate;
}

public string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}

public DateTime BirthDate
{
get
{
return _birthDate;
}
set
{
_birthDate = value;
}
}

public int CompareTo(object obj)
{
//required by IComparable
if (obj is Dog)
{
Dog temp = (Dog) obj;

return _name.CompareTo(temp.Name);
}
else
{
throw new ArgumentException(
"Object is not a Dog");
}
}
}




You'll notice that as required by the IComparer class, Dog has a public method named "CompareTo". The ArrayList will execute this method when comparing two Dog objects during the sort algorithm.



What about non default sorts? We need a couple of IComparer objects to pass into the ArrayList. Here's some examples of objects that will compare Dog objects by Name or Birthdate:





public class DogNameComparer : IComparer
{
public int Compare(object x, object y)
{
if (x is Dog && y is Dog)
{
Dog dog1 = (Dog) x;
Dog dog2 = (Dog) y;
return dog1.Name.CompareTo(dog2.Name);
}
else
{
throw new ArgumentException(
"Object is not a dog.");
}
}
}






public class DogBirthDateComparer : IComparer
{
public int Compare(object x, object y)
{
if (x is Dog && y is Dog)
{
Dog dog1 = (Dog) x;
Dog dog2 = (Dog) y;

//let the underlying data type do the work:
return dog1.BirthDate.CompareTo(dog2.BirthDate);
}
else
{
throw new ArgumentException("Object is not a Dog");
}
}
}





Now on to that collection. We implement a pretty standard collection, including a couple of handy overloads for the Add method. We also add a "Sort" method to tell our collection to sort itself. Note that the Sort method makes the underlying ArrayList do the heavy lifting.





public class DogCollection : CollectionBase
{
public DogCollection()
{
//Empty Constructor
}

//Add this dog directly to the inner list
//The inner list is an array list, and we'll
//use it for sorting later
public virtual Dog Add()
{
Dog newDog = new Dog();
this.InnerList.Add(newDog);
return newDog;
}

public virtual void Add(Dog dog)
{
this.InnerList.Add(dog);
}

public virtual Dog Add(string name, DateTime birthDate)
{
Dog newDog = new Dog(name, birthDate);
this.InnerList.Add(newDog);
return newDog;
}

public void Sort()
{
//easy as pie - since no sort type was specified,
//the InnerList ArrayList object will use the
// implementation of IComparable built into the
//dog object itself

this.InnerList.Sort();
}

public void Sort(DogSortType type)
{
//since a sort type was specified,
//we'll use one of our Sorter objects
//in conjunction with the array list to
//do the sort

switch (type)
{
case DogSortType.Name:
{
this.InnerList.Sort(new DogNameComparer());
break;
}

case DogSortType.BirthDate:
{
this.InnerList.Sort(new DogBirthDateComparer());
break;
}

default:
{
throw new ArgumentException(
"Unsupported sort type: "
+ type.ToString());
}
}
}
}





And finally, a little bit of enumeration to define the types of sorting allowed:




public enum DogSortType
{
Name,
BirthDate
}




Viola! It's cake.



Watch for updates to this post (there are details I still need to add), but one thing I would point out is to ALWAYS CODE DEFENSIVELY. For example, you'll notice that the methods in these objects don't trust the data passed in - they all test for invalid values, and take appropriate action.



Happy coding!

No comments: