Wednesday, April 30, 2008

Generic reordering

Do you have items in your application that need to be ordered in some arbitrary, user defined order? Countries could very well be ordered alphabetically, but let's assume that one of our requirements is for arbitrarily ordered countries. Here's a user interface for this. The little green up and down arrows can be clicked to reorder the countries:

IOrderable

Here's the country table, note that it's got a position field that tells us the position of the country.

CountryTable

So, when I click an up arrow I just need to find the country above's position number and swap the current position number with it. This is all standard stuff. But what I want to show you today is how we can make a generic ordering component that can do all this stuff for us for any entity that implements an IOrderable interface:

public interface IOrderable
{
   int Position { get; set; }
}

Here's a snippet of my CountryController, note that I've got an instance of IOrderableService<Country> and two actions, 'MoveUp' and 'MoveDown'. All these two actions have to do is call MoveItemAtPosition(x).UpOne() or DownOne() in order to reorder the country.

public class CountryController : ControllerBase
{
 IOrderableService<Country> countryOrderableService;

 public CountryController(IOrderableService<Country> countryOrderableService)
 {
  this.countryOrderableService = countryOrderableService;
 }

 public ActionResult MoveUp(int id)
 {
  countryOrderableService.MoveItemAtPosition(id).UpOne();
  return RenderIndexView();
 }

 public ActionResult MoveDown(int id)
 {
  countryOrderableService.MoveItemAtPosition(id).DownOne();
  return RenderIndexView();
 }
}

This is one of the cool side effects of having a generic repository. See my previous post on Using the IRepository pattern with LINQ to SQL. Because of the generic repository we can write a generic ordering service for any entity.

Now consider these categories:

Categories

These are orderable but also nested. We only want to order an item within it's nested level. Here's a snippet of the category controller:

public class CategoryController : ControllerBase
{
   IOrderableService<Category> orderableService;

   public CategoryController(
       IOrderableService<Category> orderableService)
   {
       this.orderableService = orderableService;
   }

   public ActionResult MoveUp(int id)
   {
       Category category = categoryRepository.GetById(id);
       orderableService
           .MoveItemAtPosition(category.Position)
           .ConstrainedBy(c => c.ParentId == category.ParentId).UpOne();
       return RenderIndexView();
   }

   public ActionResult MoveDown(int id)
   {
       Category category = categoryRepository.GetById(id);
       orderableService
           .MoveItemAtPosition(category.Position)
           .ConstrainedBy(c => c.ParentId == category.ParentId).DownOne();
       return RenderIndexView();
   }
}

As you can see, this time we've got the same IOrderableService<T>, but now we're using ConstrainedBy and telling the service that we only want to reorder within the set of categories that have the same parent id. Since the constraint is a lambda expression we can have considerable scope for defining the constraint. In another part of the application products are constrained by their categoryId when reordering.

All the code for these examples is in my suteki shop application that you can check out from google code here:

http://code.google.com/p/sutekishop/

Have a look at the OrderableService and Move classes. You can see Suteki Shop in action here:

http://sutekishop.co.uk/home

Happy ordering!

No comments: