Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / sorting

User sortable listbox

4.83/5 (5 votes)
31 Jan 2012CPOL5 min read 22K   964  
A listbox control that allows reordering of items using drag'n'drop.

UserSortableListboxExample

Introduction

This control allows the user to reorder ListBox items using a drag'n'drop method. It supports the standard Items property (with strings as well as custom objects) and the DataSource property, if the data source implements the IList interface (i.e., BindingList).

Background

There are several code snippets in the web that allow the user to sort a listbox, but most of them support only a string list (or any other that is hard-coded). My approach is based on BFree's answer at StackOverflow: http://stackoverflow.com/a/805267/540761.

Why the native approach is not enaugh?

The code from the link above is simple and good as long as we use only the Items property with strings (or, more generally, with one particular type). Sometimes it is nice to use a DataSource (for example, to use the same items list for many controls) with items other than strings. It is also a good idea to have a control that will be item-type independent. To achieve that, we will create a UserSortableListbox class that inherits from System.Windows.Forms.ListBox.

To keep things simple

I decided to assume two important things to keep the code simple:

  • UserSortableListbox is always user-sortable (there is no possibility to disable drag and drop)
  • The only supported SelectionMode is SelectionMode.One.

Both functionality can be quite easily implemented and will be described later.

Drag and drop

To allow the user to reorder items using drag and drop, we have to handle three events: beginning of the reorder (MouseDown event), moving the element (DragOver), and dropping an item (DragDrop).

We start the drag and drop mechanism after the MouseDown event:

C#
protected override void OnMouseDown(MouseEventArgs e)
{
    base.OnMouseDown(e);
    if (SelectedItem == null)
    {
        return;
    }
    sourceIndex = SelectedIndex;
    OnSelectedIndexChanged(e); //(*)
    DoDragDrop(SelectedItem, DragDropEffects.Move);
}

sourceIndex is simply defined somewhere in the class:

C#
private int sourceIndex = -1;

The only thing that needs explanation here is OnSelectedIndexChanged(e). We need this because SelectedIndexChanged will not be launched when we handle MouseDown.

Handling the move of an item is trivial:

C#
protected override void OnDragOver(DragEventArgs e)
{
    base.OnDragOver(e);
    e.Effect = DragDropEffects.Move | DragDropEffects.Scroll;
}

Now, the most interesting part. After the DragDrop event occurs, we can do the job and move the dropped item to the proper place. Here is the first version of the OnDragDrop method:

C#
protected override void OnDragDrop(DragEventArgs e)
{
    base.OnDragDrop(e);
    //(1)
    Point point = PointToClient(new Point(e.X, e.Y));
    int index = IndexFromPoint(point); //destination index
    //(2)
    if (index < 0) index = Items.Count - 1;
    
    //(3a)
    if (index > sourceIndex)
    {
        Items.Insert(index + 1, Items[sourceIndex]);
        Items.RemoveAt(sourceIndex);
    }
    //(3b)
    else
    {
        Items.Insert(index, Items[sourceIndex]);
        Items.RemoveAt(sourceIndex + 1);
    }
    //(4)
    SelectedIndex = index;
}

Some comments to this code:

  1. We don't have a simple way to indicate a new index of the dropped item, as in the OnMouseDown method. However, we can use the inherited IndexFromPoint method, which will give us what we are looking for. The only thing we must remember is to transform e.X and e.Y to client coordinates.
  2. In this line, we have to decide how we will handle dropping the item below the last element in the listbox (because you cannot drag an item outside the listbox, the only situation where IndexFromPoint returns -1 will be when the user drops an item below the last one). The most intuitive way to handle this situation is to set the destination index as the last index in the list.
  3. When we have a source and destination index, we can move items. First, we copy an item by inserting Items[sourceIndex] once again in Items, and then we remove the 'original one'. If the destination index is greater than (below) the source, removing from sourceIndex will affect the destination index, so we are inserting at index + 1. Similarly, when the destination index is less than (above) the source, inserting at the position index will affect sourceIndex, so we have to remove at sourceIndex + 1.
  4. We remove the previously selected item, so it is time to reselect it at its new position.

We have recreated the basic solution. The only advantage is that there is no more e.Data.GetData() in the code. Luckily enough, adding DataSource support is really simple now. We just have to find a common class (or interface) for DataSource and the Items field that will let us manipulate its elements, especially provide the Count, Insert, and RemoveAt methods. Items has the ObjectCollection type, which implements IList, ICollection, and IEnumerable. Because the IList interface is exactly what we are searching for and we can assume that our DataSource will implement it, we will create a variable with this type called items and replace all Items with items in the OnDragDrop method, which will do the job and allow us to use DataSource in UserSortableListbox.

C#
IList items = DataSource != null ? DataSource as IList : Items;

More functionality

To make the control more useful, we can add a Reorder event, which will be fired when the user moves an item:

C#
public class ReorderEventArgs : EventArgs
{
    public int index1, index2;
}
public delegate void ReorderHandler(object sender, ReorderEventArgs e);
public event ReorderHandler Reorder;

index1 and index2 are the source and destination indices of the moved item. Here is the complete OnDragDrop method, including DataSource support and Reorder event:

C#
protected override void OnDragDrop(DragEventArgs e)
{
    base.OnDragDrop(e);
    Point point = PointToClient(new Point(e.X, e.Y));
    int index = IndexFromPoint(point);
    IList items = DataSource != null ? DataSource as IList : Items;
    if (index < 0) index = items.Count - 1;
    if (index != sourceIndex)
    {
        if (index > sourceIndex)
        {
            items.Insert(index + 1, items[sourceIndex]);
            items.RemoveAt(sourceIndex);
        }
        else
        {
            items.Insert(index, items[sourceIndex]);
            items.RemoveAt(sourceIndex + 1);
        }
        if (null != Reorder)
            Reorder(this, new ReorderEventArgs() { index1 = sourceIndex, index2 = index });
    }
    SelectedIndex = index;
}

Keep the implementation simple

As I mentioned above, we assumed drag and drop cannot be disabled and only SelectionMode.One would be available while using the control, so we should hide AllowDrop and SelectionMode from the Designer and set the proper values in the constructor:

C#
[Browsable(false)]
new public bool AllowDrop
{
    get { return true; }
    set { }
}
[Browsable(false)]
new public SelectionMode SelectionMode
{
    get { return SelectionMode.One; }
    set { }
}
public UserSortableListBox() //this is the constructor
{
    base.AllowDrop = true;
    base.SelectionMode = SelectionMode.One;
}

Of course, we can add support for those properties we had just disabled. If you want to allow disabling moving items, you just need to check AllowDrop (or another new property) at the beginning of OnMouseMove and do or don't DoDragDrop().

Supporting other selection modes is more complicated, but still simple. Instead of moving an item and have a sourceIndex, we would have to add a sourceIndex[] array, which would be copied from SelectedIndices in OnMouseDown, and primarySourceIndex, which will contain the clicked item (also in OnMouseDown, it can be obtained from IndexFromPoint without the need for transforming coordinates). Then, in the OnDragDrop method, we move all items using (primarySourceIndex - index) positions: item at sourceIndex[i] will be moved to the sourceIndex[i] + primarySourceIndex - index position.

Using the code

Using this control is as simple as using the standard ListBox. The Reorder event is available in the Designer and can be handled easily.

C#
userSortableListBox1.Reorder += 
  new synek317.Controls.UserSortableListBox.ReorderHandler(this.optionsListBox_Reorder);
void optionsListBox_Reorder(object sender, UserSortableListBox.ReorderEventArgs e)
{
  //moved index is at e.index2 position
  //or simply at userSortableListBox1.SelectedIndex
  //previously it was at e.index1 position
}

In the downloads section, I have included a .dll file with the compiled control so you can just add it to your project references and use it out of the box.

Points of interest

I'm not sure why SelectedIndexChanged is not launched when the MouseDown event is used for the listbox. However, I used a workaround and my control simply launches the SelectedIndexChanged event from MouseDown.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)