Ordering child items with NHibernate

I recently had to implement an ordered collection of items within it's parent. I stumbled around for a while trying to get he FluentNHibernate mapping to work.
January 24 2013

It’s common to need to have an ordered list of something.  In my application I have a Todo List which has ordered items that need to be done, Todo List Items.  I want the user to be able to re-order the list of todo items as their priority changes.   At first glance I thought the TodoListItem needed a Position property that denoted it’s position within the list.  Using this implementation I would tell the TodoList to change the position of the TodoListItem to a particular position, which would likely mean other TodoListItems position would need updating. 

For example, if a TodoListItem had position = 3 and I wanted to make it position = 5, I would need to move items at positions 5 and 4 down one.  Similar adjustments would have to be made when items are removed.  (Added items are added to the end of the list, meaning no other items need to change).

I got this solution working but I didn’t like that a developer could set todoListItem.Position and that would muck up other items in the list if he didn’t remember to change the effected items also.  In short it was a leaky API.

Lists have an implied order so I wanted to make use of this and let NHibernate take care of updating the value of Position.  I at first removed the Position property from TodoListItem, thinking it not necessary if I can calculate the position from the it’s place in the list.

To cut to the solution (which is what we’re all after), after some experimentation and failure I arrived at the following working solution.

Model

public class TodoList
{
    private IList<TodoListItem> _todoListItems;
    
    public TodoList()
    {
        _todoListItem = new List<TodoListItem>();
    }

    public virtual IEnumerable<TodoListItem> TodoItems { get { _todoListItem; } }

    public virtual int GetPositionOf(TodoListItem item)
    {
        return _todoListItems.IndexOf(item);
    }

    /* Other fields removed for brevity */
}

public class TodoListItem
{
    public virtual TodoList TodoList { get; set; } 
    public virtual int Position {
        get 
        {
            // Will Exception here if TodoList is null
            // Use whatever gaurds suit your needs
            return TodoList.GetPositionOf(this);
        }
        protected set { } 
    }
}

 

You’ll notice I’ve included the Position property on TodoListItem but it’s calculated from the TodoList (good!).   What is a little unexpected (for me) is that I need to have a protected set method that does nothing.  My guess is the Dynamic Proxy created by NHibernate needs this to persist the Position field.  Certainly without it your application will nearly work, just the Position column in the table won’t be updated, and new TodoListItem’s cannot be added.

Now for the FluentNHibernate mappings.

Fluent NHibernate Mapping

public class TodoListMap : ClassMap<TodoList>
{
    public TodoListMap()
    {
        /* Other mappings omitted for brevity */

        HasMany(x => x.Items)
            .AsList(part => part.Column("Position")
            .KeyColumn("TodoListId")
            .Access.CamelCaseField(Prefix.Underscore)
            .Inverse()
            .Cascade.AllDeleteOrphan();
    }
}

public class TodoListItemMap : ClassMap<TodoListItem>
{
    public TodoListItemMap()
    {
        /* Other mappings omitted for brevity */

        // Needed or the Position will not save to the db
        // Even though we never explicitly set it in our code
        Map(x => x.Position);

        References(x => x.TodoList).Not.Nullable();
    }
}

 

I’m not sure HOW the Position field is being updated.  There’s some stronger magic in play than I know.  However, it definitely does work. Now the only way the order of Todo List Items can be set is via the TodoList owning entity and all other TodoListItems are updated as needed, with me having to manually updated their Position. MAGIC!

Post a comment

comments powered by Disqus