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

Drag-and-Drop ListBox

4.93/5 (33 votes)
18 May 2009CPOL14 min read 198K   7.3K  
DragDropListBox, a control derived from ListBox allowing drag-and-drop in multiselect mode.

Image 1

Introduction

As I tried to teach a ListBox how to do drag-and-drop with multiselect enabled, I soon realized that there was no simple solution to this problem. In fact, all the sources on this subject I found on the internet (including Microsoft) are telling the same story: "It is not possible to use drag-and-drop with multiselect in ListBoxes!".

Yet, I found a way to do it.

The WinForms controls contain a very basic infrastructure for drag-and-drop, but they don't provide drag-and-drop functionality right out of the box. You still have to do a lot of (not very obvious) coding to make drag-and-drop come alive.

In this article, I will show you how to create a new control called DragDropListBox which does drag-and-drop right away and which solves the multiselect problem.

Features

We want to create a control by deriving it from ListBox. It should support the following features without additional coding:

  • Drag-and-drop between different list boxes.
  • Drag-and-drop within one list box in order to reorder the items.
  • Drag-and-drop several items at a time (not obvious with ListBox).
  • Fine-tuning of the drag-and-drop behavior at design time through the properties window.
  • Provide visual feedback.

Subclassing

Deriving a class from another class is called subclassing. Let's create a new control called DragDropListBox by subclassing ListBox:

C#
public class DragDropListBox : ListBox
{
    // TODO: Enhance ListBox!
}

We'll have to write event code for different mouse and drag-and-drop events. When subclassing controls, this is not done by attaching methods to the events, but by overriding the event methods of the base class:

C#
// WRONG:
this.MouseDown += new MouseEventHandler(DragDropListBox_MouseDown);
 
// RIGHT:
protected override void OnMouseDown(MouseEventArgs e)
{
    base.OnMouseDown(e);
    // Your code to go here...
}

And, don't forget to call the event method of the base class, since it contains the code which raises the event!

Fine-Tuning of the Drag-and-Drop Behavior

Several things can be done by drag-and-drop:

  • Moving items from one control to another. This removes the items from the source control.
  • Copying items from one control into another.
  • Reorder items within one control.

The user of our new control doesn't always want to allow all of these operations. If one of the DragDropListBoxes is used as a kind of toolbox, then the tools should be copied to the target, but not removed from the toolbox. On the other hand, if a DragDropListBox lists available options and another one lists selected options, then the selected options should be removed from the list of available options. The user also might want to disallow dropping stuff on the toolbox or changing the order of the available options.

In order to allow the fine-tuning of all these things, we introduce new boolean properties:

  • AllowReorder
  • IsDragDropCopySource
  • IsDragDropMoveSource
  • IsDragDropTarget

Another problem might occur, if we have four or more DragDropListBoxes. As an example, let's assume that we have two pairs of DragDropListBoxes, each of them representing lists of available respectively selected items. Let's say that one pair handles cats and the other dogs. We should only be able to move cats between the two cat-DragDropListBoxes and dogs between the two dog-DragDropListBoxes. Let's introduce a string property for this purpose:

  • DragDropGroup

We want DragDropListBoxes to only allow drag-and-drop operations between DragDropListBoxes having the same DragDropGroup. Usually, this property will be an empty string. But, in this example, we could set the DragDropGroup properties of the two cat-DragDropListBoxes to "cats". We can either leave DragDropGroup empty in two dog-DragDropListBoxes or set them to "dogs", for example. They just have to be different from "cats".

As an example, here is the code for just one of these properties:

C#
private bool _isDragDropCopySource = true;
 
[Category("Behavior (drag-and-drop)"), DefaultValue(true), 
  Description("Indicates whether ...")]
public bool IsDragDropCopySource
{
    get { return _isDragDropCopySource; }
    set { _isDragDropCopySource = value; }
}

Please note the Attributes: Category places this property in a new group called "Behavior (drag-and-drop)" in the Properties window. We'll place all these five properties in this group. DefaultValue defines the default value of the property. Description is - well - a description of the property that will automatically be shown in the intellisense tooltips and in the Properties window.

We'll see the details later.

The Multiselect Problem

How does drag-and-drop work? In the extended selection mode, the user selects items by clicking an item. He may then select a range by holding the Shift-key and clicking on another item. By holding the Control-key, he can then select and unselect single items. Now, if he clicks on one of the selected items and (while keeping the left mouse button pressed) starts moving the mouse, the drag-and-drop operation is initiated.

This works perfectly with the ListView control, but not with the ListBox control. Why? Let's make a little experiment. Please select a few files in the Windows Explorer. (The Windows Explorer uses a ListView.) Point to a selected file. Press the left mouse button. Don't move the mouse. Nothing happens. Release the mouse button now. This deselects all the files but the one you clicked at. Note that the files remain selected while the mouse button is down. This is good, because the drag-and-drop operation starts while the mouse button is pressed.

However, if you repeat the same experiment with a ListBox, you will notice that the selection gets lost as soon as you press the mouse button! This is bad!

To remedy this deficiency, we need to keep track of the selection. Fortunately, the ListBox has an event called SelectedIndexChanged. This sounds exactly like what we need. Let's override the corresponding event method:

C#
protected override void OnSelectedIndexChanged(EventArgs e)
{
    base.OnSelectedIndexChanged(e);
    SaveSelection();
    // Save the selection,
    // so that we can restore it later.
}

SelectedIndexChanged fires whenever the selection changes, even if this is done programmatically. But, we don't want to save the selection while we are restoring it. Let's define a flag _restoringSelection that we will set while restoring the selection. We also need an array _selectionSave where we can store the indices of the selected items. And now, for the method SaveSelection:

C#
private int[] _selectionSave = new int[0];
private bool _restoringSelection = false;
 
private void SaveSelection()
{
    if (!_restoringSelection && SelectionMode == SelectionMode.MultiExtended) {
        SelectedIndexCollection sel = SelectedIndices;
        if (_selectionSave.Length != sel.Count) {
            _selectionSave = new int[sel.Count];
        }
        SelectedIndices.CopyTo(_selectionSave, 0);
    }
}

Nothing special here. If we are not restoring the selection and multiselect mode is enabled, then we copy the selected indices to the _selectionSave array using the CopyTo method of the SelectedIndices collection.

Note: There is also a selection mode SelectionMode.MultiSimple which selects or unselects items one by one. No need to restore the selection here. Restoring the selection would also reverse the selection change just made. It is still possible to drag multiple items with SelectionMode.MultiSimple; however, you have to start dragging while selecting the last item (since clicking on an already selected item would unselect it).

We also need a corresponding method which restores the selection:

C#
private void RestoreSelection(int clickedItemIndex)
{
    if (SelectionMode == SelectionMode.MultiExtended && 
        Control.ModifierKeys == Keys.None &&
        Array.IndexOf(_selectionSave, clickedItemIndex) >= 0) {
 
        _restoringSelection = true;
        foreach (int i in _selectionSave) {
            SetSelected(i, true);
        }
        SetSelected(clickedItemIndex, true);
        _restoringSelection = false;
    }
}

If the user wants to drag items, he will not be pressing modifier keys like Shift or Control. If he does, he is probably still editing the selection. We only restore the selection if the user clicks on an item that was already selected before. _selectionSave stores the state before the click. First, let's set the flag _restoringSelection mentioned above. We can now simply reselect the items by calling SetSelected for each stored item index. SetSelected is then called again for the item that was clicked, in order to make it the current item. (This also fixes a strange bug of ListBox, where too many items are selected, if the list is clicked after items have been selected programmatically.) Finally, let's clear the flag.

We'll see later how RestoreSelection is called in the OnMouseDown event method.

Initiating Drag-and-Drop

The drag-and-drop process has to be initiated explicitly. In order to do this, we have to detect the conditions of a starting drag operation. They are fulfilled if the user clicks on a selected item and moves the mouse by a minimum amount without releasing the mouse button. Therefore, the MouseDown, MouseUp, and the MouseMove events are involved. In OnMouseDown, we register the initial mouse position. In OnMouseUp, we detect the releasing of the mouse button. In OnMouseMove, we look how far the mouse has moved since the user clicked.

C#
private Rectangle _dragOriginBox = Rectangle.Empty;
 
protected override void OnMouseDown(MouseEventArgs e)
{
    base.OnMouseDown(e);
 
    int clickedItemIndex = IndexFromPoint(e.Location);
    if (clickedItemIndex >= 0 && MouseButtons == MouseButtons.Left &&
        (_isDragDropCopySource || _isDragDropMoveSource || _allowReorder) &&
        (GetSelected(clickedItemIndex) || Control.ModifierKeys == Keys.Shift)) {
 
        RestoreSelection(clickedItemIndex);
 
        Size dragSize = SystemInformation.DragSize;
        _dragOriginBox = new Rectangle(new Point(e.X - 
          (dragSize.Width / 2), e.Y - (dragSize.Height / 2)), dragSize);
    }
}
 
protected override void OnMouseUp(MouseEventArgs e)
{
    base.OnMouseUp(e);
    _dragOriginBox = Rectangle.Empty;
}
 
protected override void OnMouseMove(MouseEventArgs e)
{
    base.OnMouseMove(e);
    if (_dragOriginBox != Rectangle.Empty && 
             !_dragOriginBox.Contains(e.X, e.Y)) {
        DoDragDrop(new DataObject("IDragDropSource", this), 
                   DragDropEffects.All);
        _dragOriginBox = Rectangle.Empty;
    }
}

In OnMouseDown, we test if all the preconditions are met before we do anything. First, let's look whether the user has clicked on an item. The ListBox method IndexFromPoint returns a positive index value if the coordinates handed over lie within a list item. Did the user press the left mouse button? Also, our fine-tuning properties described above must allow our list box to be either a possible source for drag-and-drop operations or they must allow the reordering of list items. The item clicked at must be selected. If Shift is pressed, GetSelected does not return the correct value (I don't know why), but then the item is selected anyway. Figuring out all these conditions in OnMouseDown and in RestoreSelection took me a lot of time. The whole thing is really tricky, since ListBox does not always behave as expected.

If these conditions are met, we can go on. As explained before, clicking into the selection might destroy it - let's restore it by calling our method RestoreSelection. Because we want to know if the mouse has moved enough, we define a Rectangle _dragOriginBox that defines the bounds the mouse has to cross, before we actually initiate the drag-and-drop process. SystemInformation.DragSize tells us how large this rectangle has to be. The rectangle is centered on the clicking position.

In OnMouseUp, we look if the user releases the mouse button. If he does so before drag-and-drop is initiated, the game is over so far. Setting our rectangle to Rectangle.Empty tells OnMouseMove not to initiate drag-and-drop.

In OnMouseMove, we look if the rectangle has been defined and if the mouse crossed the bounds. If so, we can initiate the drag-and-drop process by calling DoDragDrop. This method expects data that will be passed to the drop target. We can pass any data we want. More on this later. Resetting our rectangle to Rectangle.Empty prevents us from reinitiating the drag-and-drop process.

Reflection

It's time to think things over. Drag-and-drop establishes a communication link between the source control and the target control. As said before, the source control can pass any information to the target control. But, what does the target control need to know from the source control? Well, of course, it wants to know which items are being dropped. But, that's not enough! Does the source control allow moving/copying, and is it in the same DragDropGroup? If the items have to be moved, there must be a way to remove them from the source. Finally, the target needs a way to tell the source to raise an event when the drop operation is completed.

A simple way to satisfy the drop target would be to give it a reference to the source DragDropListBox. This has, however, the disadvantage that drag-and-drop would only work between two DragDropListBoxes. How can we extend the drag-and-drop capabilities to other controls? We could create a dedicated data class that would transport the requested information. However, a simpler and more universal approach is to leave this information in our DragDropListBox class, but to regroup the related methods and properties into an interface:

C#
public interface IDragDropSource
{
    string DragDropGroup { get; }
    bool IsDragDropCopySource { get; }
    bool IsDragDropMoveSource { get; }
    
    object[] GetSelectedItems();
    void RemoveSelectedItems(ref int rowIndexToAjust);
    void OnDropped(DroppedEventArgs e);
}
 
public class DragDropListBox : ListBox, IDragDropSource
{
    // ...
}

Now, when initiating the drag-and-drop process, we still pass a DragDropListBox reference, but the drop target treats it as an IDragDropSource (the data we pass to the DoDragDrop method must be packed in a DataObject):

C#
// The source control initiates drag-and-drop in OnMouseMove:
DoDragDrop(new DataObject("IDragDropSource", this), 
           DragDropEffects.All);
 
// The target control retrieves the information in OnDragDrop:
IDragDropSource src = drgevent.Data.GetData("IDragDropSource") 
                      as IDragDropSource;

Any control which implements IDragDropSource could serve as a drag-and-drop source now.

We have already implemented the properties of IDragDropSource, but we still have to provide an implementation for the methods:

C#
public object[] GetSelectedItems()
{
    object[] items = new object[SelectedItems.Count];
    SelectedItems.CopyTo(items, 0);
    return items;
}
 
public void RemoveSelectedItems(ref int itemIndexToAjust)
{
    for (int i = SelectedIndices.Count - 1; i >= 0; i--) {
        int at = SelectedIndices[i];
        Items.RemoveAt(at);
        if (at < itemIndexToAjust) {
            itemIndexToAjust--;
        }
    }
}

public virtual void OnDropped(DroppedEventArgs e)
{
    var dropEvent = Dropped;
    if (dropEvent != null) {
        dropEvent(this, e);
    }
}

What is the strange ref-parameter itemIndexToAjust in RemoveSelectedItems? If items are being reordered (instead of moved or copied), the source and the target control are identical. If the items to remove lie before the insert point, then the insert point changes. We could, of course, first insert the items at the new location and then remove them from the old location. But, inserting items could change the index of the (possibly many) items to remove. It's easier to start with removing. Therefore, we pass the index of the intended insert point to RemoveSelectedItems as a ref-parameter and ask RemoveSelectedItems to adjust it for us. We also take care to remove the items backwards, in order to preserve the index of the not yet removed items.

OnDropped is called when a drag-and-drop operation is completed in order to raise the Dropped event. The event itself is declared as:

C#
[Category("Drag Drop"), 
  Description("Occurs when a extended DragDropListBox " + 
              "drag-and-drop operation is completed.")]
public event EventHandler<DroppedEventArgs> Dropped;

The DroppedEventArgs are defined as follows:

C#
public enum DropOperation
{
    Reorder,
    MoveToHere,
    CopyToHere,
    MoveFromHere,
    CopyFromHere
}

public class DroppedEventArgs : EventArgs
{
    public DropOperation Operation { get; set; }
    public IDragDropSource Source { get; set; }
    public IDragDropSource Target { get; set; }
    public object[] DroppedItems { get; set; }
}

Visual Feedback

I mentioned the visual cue before. It simply consists of a horizontal line that appears when dragging items over a DragDropListBox to indicate the drop position.

DragDropListBox4.gif

Drawing this line is easy, but removing it is tricky. Removing a visual element means to redraw what this element is hiding. My first attempt was to just redraw the white background. But, this doesn't work well. The list might have selected elements with a colored background. I chose to draw the visual cue line two pixels wide. So, it might be partially on a normal and partially on a selected background. I also might hide parts of the text. Especially, descenders of characters (like y or g).

In old versions of VB, there was the option to draw in inverted mode. Inverting the color of the background always provided a good contrast, even on heterogeneous backgrounds. And, what's more important, by drawing the same thing a second time, the original appearance was automatically restored. That's what we need! But unfortunately, there is no inverting Pen or Brush in System.Drawing; however, this can be achieved by using functions provided by the Win32 API.

We also have to figure out the exact position for the visual cue and we need to remember its position in order to be able to remove it later. I decided to put all these functions in a new class called VisualCue. Here is VisualCue (a bit shortened):

C#
public class VisualCue
{
    public const int NoVisualCue = -1;
 
    public VisualCue(ListBox listBox) { _listBox = listBox; }
 
    public void Clear()
    {
        if (_index != NoVisualCue) {
            Draw(_index);
            // Draws in inverted mode and
            // thus deletes the visual cue;

            _index = NoVisualCue;
        }
    }
    
    public void Draw(int itemIndex)
    {
        // ...   
        // Get the coordinates of the line
        if (_listBox.Sorted) { 
            // Let's draw a vertical line on the
            // left of the list if the list is sorted, 
            // since items could just be dropped anywhere.
 
            rect = _listBox.ClientRectangle;
            // ...
        } else { 
            rect = _listBox.GetItemRectangle(itemIndex);
            // ...
        }
        IntPtr hdc = Win32.GetDC(IntPtr.Zero); // Get device context.
        Win32.SetROP2(hdc, Win32.R2_NOT); // Switch to inverted mode.
        Win32.MoveToEx(hdc, l1p1.X, l1p1.Y, IntPtr.Zero);
        Win32.LineTo(hdc, l1p2.X, l1p2.Y);
        // ...
        Win32.ReleaseDC(IntPtr.Zero, hdc); // Release device context.
        _index = itemIndex;
    }
 
    public int Index { get { return _index; } }
}

This class on its part relies on a static class Win32 containing all the API declarations. The normal drawing methods of System.Drawing do not work in inverted mode, so we need to use API functions for all the drawings.

And Drag-and-Drop Goes On!

We still have a lot to do in order to make drag-and-drop work. We did not handle the drag-and-drop events until now. Before we do so, let's define two helper methods.

GetDragDropEffect determines the drag-and-drop operation which is being performed, which can be either None, Move, or Copy.

C#
private DragDropEffects GetDragDropEffect(DragEventArgs drgevent)
{
    const int CtrlKeyPlusLeftMouseButton = 9; // KeyState.
 
    DragDropEffects effect = DragDropEffects.None;
 
    // Retrieve the source control
    // of the drag-and-drop operation.
    IDragDropSource src = 
      drgevent.Data.GetData("IDragDropSource") as IDragDropSource;
 
    if (src != null && _dragDropGroup == src.DragDropGroup) {
    // The stuff being draged is compatible.
        if (src == this) {
        // Drag-and-drop happens within this control.
            if (_allowReorder && !this.Sorted) {
            // We can not reorder, if list is sorted.
                effect = DragDropEffects.Move;
            }
        } else if (_isDragDropTarget) {
            // If only Copy is allowed then copy. If Copy and Move
            // are allowed, then Move, unless the Ctrl-key is pressed.
            if (src.IsDragDropCopySource && 
               (!src.IsDragDropMoveSource || drgevent.KeyState == 
                  CtrlKeyPlusLeftMouseButton)) {
                effect = DragDropEffects.Copy;
            } else if (src.IsDragDropMoveSource) {
                effect = DragDropEffects.Move;
            }
        }
    }
    return effect;
}

DropIndex gets the index of the item before which items are being dropped. The index is calculated from the vertical position of the mouse. If the drop position lies after the last item in the list, then the index of the last item + 1 (which is equal to Item.Count) is returned instead.

C#
private int DropIndex(int yScreen)
{
    // The DragEventArgs gives us screen coordinates.
    // Convert the screen coordinates to client coordinates.
    int y = PointToClient(new Point(0, yScreen)).Y;
 
    // Make sure we are inside of the client rectangle.
    // If we are on the border of the ListBox,
    // then IndexFromPoint does not return a match.
    if (y < 0) {
        y = 0;
    } else if (y > ClientRectangle.Bottom - 1) {
        y = ClientRectangle.Bottom - 1;
    }
 
    int index = IndexFromPoint(0, y);
    // The x-coordinate doesn't make any difference.

    if (index == ListBox.NoMatches) {
    // Not hovering over an item
        return Items.Count;
        // Append to the end of the list.
    }
 
    // If hovering below the middle of the item,
    // then insert after the item.
    Rectangle rect = GetItemRectangle(index);
    if (y > rect.Top + rect.Height / 2) {
        index++;
    }
 
    int lastFullyVisibleItemIndex = TopIndex + 
                ClientRectangle.Height / ItemHeight;
    if (index > lastFullyVisibleItemIndex) {
    // Do not insert after the last fully visible item
        return lastFullyVisibleItemIndex;
    }
    return index;
}

When entering the drop target with the mouse, we have to set the right mouse cursor by setting the drag-drop-effect.

DragDropListBox3.gif No dropping here! DragDropListBox1.gif Copy DragDropListBox2.gif Move

When leaving the drop target, we need to remove the visual cue, if any had been drawn meanwhile.

C#
protected override void OnDragEnter(DragEventArgs drgevent)
{
    base.OnDragEnter(drgevent);
    drgevent.Effect = GetDragDropEffect(drgevent);
}
 
protected override void OnDragLeave(EventArgs e)
{
    base.OnDragLeave(e);
    _visualCue.Clear();
}

While moving the mouse over the drop target, we have to continuously adjust the look of the mouse cursor and the visual cue:

C#
protected override void OnDragOver(DragEventArgs drgevent)
{
    base.OnDragOver(drgevent);
 
    drgevent.Effect = GetDragDropEffect(drgevent);
    if (drgevent.Effect == DragDropEffects.None) {
        return;
    }
 
    // Everything is fine, give a visual cue
    int dropIndex = DropIndex(drgevent.Y);
    if (dropIndex != _visualCue.Index) {
        _visualCue.Clear();
        _visualCue.Draw(dropIndex);
    }
}

And now, for the last method! Here is where the dropping actually happens. We have to:

  • Clear the visual cue
  • Retrieve the drag item data
  • Care about the sorting state of the list
  • Insert the dropped items
  • Remove all the selected items from the source (if moving)
  • Adjust the selection in the target
  • Raise the Dropped event in the target
  • Raise the Dropped event in the source
C#
protected override void OnDragDrop(DragEventArgs drgevent)
{
    base.OnDragDrop(drgevent);
 
    _visualCue.Clear();
 
    // Retrieve the drag item data. 
    // Conditions have been testet in OnDragEnter
    // and OnDragOver, so everything should be ok here.
    IDragDropSource src = 
      drgevent.Data.GetData("IDragDropSource") 
      as IDragDropSource;
    object[] srcItems = src.GetSelectedItems();
 
    // If the list box is sorted, we don't know
    // where the items will be inserted
    // and we will have troubles selecting the inserted
    // items. So let's disable sorting here.
    bool sortedSave = Sorted;
    Sorted = false;
 
    // Insert at the currently hovered row.
    int row = DropIndex(drgevent.Y);
    int insertPoint = row;
    if (row >= Items.Count) {
    // Append items to the end.
        Items.AddRange(srcItems);
    } else { // Insert items before row.
        foreach (object item in srcItems) {
            Items.Insert(row++, item);
        }
    }
 
    // Remove all the selected items from the source, if moving.
    DropOperation operation;
    // Remembers the operation for the event we'll raise.

    if (drgevent.Effect == DragDropEffects.Move) {
        int adjustedInsertPoint = insertPoint;
        src.RemoveSelectedItems(ref adjustedInsertPoint);
        if (src == this) { // Items are being reordered.
            insertPoint = adjustedInsertPoint;
            operation = DropOperation.Reorder;
        } else {
            operation = DropOperation.MoveToHere;
        }
    } else {
        operation = DropOperation.CopyToHere;
    }
 
    // Adjust the selection in the target.
    ClearSelected();
    if (SelectionMode == SelectionMode.One) {
    // Select the first item inserted.
        SelectedIndex = insertPoint;
    } else if (SelectionMode != SelectionMode.None) {
    // Select the inserted items.
        for (int i = insertPoint; i < insertPoint + 
                srcItems.Length; i++) {
            SetSelected(i, true);
        }
    }
 
    // Now that we've selected the inserted items,
    // restore the "Sorted" property.
    Sorted = sortedSave;

    // Notify the target (this control).
    DroppedEventArgs e = new DroppedEventArgs() {
        Operation = operation,
        Source = src,
        Target = this,
        DroppedItems = srcItems
    };
    OnDropped(e);

    // Notify the source (the other control).
    if (operation != DropOperation.Reorder) {
        e = new DroppedEventArgs() {
            Operation = operation == DropOperation.MoveToHere ? 
              DropOperation.MoveFromHere : DropOperation.CopyFromHere,
            Source = src,
            Target = this,
            DroppedItems = srcItems
        };
        src.OnDropped(e);
    }
}

Note that the Dropped-event is raised for the target, as well as for the source control if we moved or copied. But, for the source, the MoveToHere and CopyToHere drop operations are changed to MoveFromHere and CopyFromHere, respectively.

Using the Code

The DragDropListBox can be used without any additional coding. Add the DragDropListBox to the Toolbox by right clicking in the Toolbox and executing "Choose Items..." in the shortcut menu. Then, click the "Browse..." button and select Oli.Controls.dll. The DragDropListBox should appear at the bottom of the "All Windows Forms" tab of the Toolbox. Drop it on your Form and set the properties in the Properties window. When you are developing a custom or user control in Visual Studio 2008, the new control appears automatically in the Toolbox.

Drag-and-drop doesn't work if you set the DataSource property of the DragDropListBox, because items can't be inserted directly in a ListBox when data binding is used.

Conclusion

We started with a simple idea and ended up with a quite complicated code. As so often the devil hides in the details. But now, we have a nice control and a base for a little drag-and-drop framework which can be extended to other controls.

License

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