Click here to Skip to main content
15,881,248 members
Articles / Programming Languages / C#

Manual Reordering of Items inside a ListView

Rate me:
Please Sign up or sign in to vote.
4.79/5 (19 votes)
9 Jan 2008Ms-PL6 min read 109.2K   5.2K   68   15
This article shows how to implement drag&drop inside a ListView and how to enable custom painting in a ListView.

Sample Image - ListViewCustomReorder.gif

Introduction

In another community, someone asked whether it is possible to implement something like drag&drop inside a ListView.

The person claimed to have searched for an example but didn't find any. Only articles dealing with dragging items into or out of a ListView, but not dragging and dropping items within the same ListView.

This question raised my interest, so I built a quick&dirty solution to try it out. After the basics worked, I added a few "bells and whistles" (like showing the insertion point in the ListView), but we'll come to this later on.

Basic Idea

Each drag and drop operation is mainly just a combination of handling three events:

  • MouseDown: Find out if something that can be dragged has been clicked on, and save the reference for further use.
  • MouseMove: Show the user some feedback whether or not the item can be dropped at the mouse position.
  • MouseUp: Finalize the drag operation and feed the item to the target.

System.Windows.Forms.Control has some methods and events to deal with drag and drop operations. If you want to dig deeper into drag and drop, you should read about Control.DoDragDrop(), Control.GiveFeedback(), and the Drag* events. MSDN has some nice tutorials on this topic.

Implementation - First Leg

The first thing we have to do is to remember which item is being dragged:

C#
// The LVItem being dragged
private ListViewItem _itemDnD = null;

I chose a ListViewItem so that we can easily access all its properties later on. Likewise, you could just save the index of the item as an int, but then, you'd always have to access the ListView's Item collection, and once the item has been removed from the ListView, you won't find it at all.

Also, remember that _itemDnD is just a reference to the object, so the memory consumption is minimal, as well.

Like stated above, to achieve our goal, we have to handle the following events of our ListView.

MouseDown

The ListView has a very convenient method to find the ListViewItem at a certain location: GetItemAt(), so we'll use it to find out which item the user has clicked on.

C#
_itemDnD = listView1.GetItemAt(e.X, e.Y);
// if the LV is still empty, no item will be found anyway,
// so we don't have to consider this case

Not much else to do here - just remember which item was clicked on, and you're done.

MouseMove

Once the user actually moves the mouse while having pressed a mouse button, the actual drag operation is performed.

C#
if (_itemDnD == null)
    return;

// Show the user that a drag operation is happening
Cursor = Cursors.Hand;

For simplicity reasons, I just chose one of the predefined Cursors to show that a dragging operation is in progress.

For a start, that's sufficient as well. We'll give additional feedback later on.

MouseUp

Now, we're at the point where the actual work is done. The user has released the mouse button, so we have to find out where the item has been dropped. The GetItemAt() method is called once again. Afterwards, the item is removed from the ListView and inserted at the new location.

C#
if (_itemDnD == null)
    return;
 
// use 0 instead of e.X so that you don't have
// to keep inside the columns while dragging
ListViewItem itemOver = listView1.GetItemAt(0, e.Y);
 
if (itemOver == null)
    return;
 
listView1.Items.Remove(_itemDnD);
listView1.Items.Insert(itemOver.Index, _itemDnD);
 
Cursor = Cursors.Default;

The only problem is that this algorithm isn't sophisticated enough to decide whether to insert before or after the item we dropped our ListViewItem on. Right now, we're always inserting before the item we released the mouse over.

There's also a problem now when we drop the item on itself: first, it's being removed from the Items collection, and then we want to insert it before itself. At this moment, the item has an Index of -1 (i.e., it isn't part of an Items collection), so we'll get an exception here that we cannot insert a ListViewItem before Index position -1.

Don't worry, we'll fix this later on.

First Summary

Apart from the few flaws I mentioned above, we've already implemented drag and drop operations within a ListView. You see - it's not that difficult.

Now, on to the "bells and whistles" I mentioned earlier:

Implementation - Second Leg

Insertion Before/After an Item

Right now, we always insert before the item we released the mouse button over, so it's not possible to make an item become the last item of the ListView.

To improve the handling, I chose to find out whether the item has been dropped on the upper or lower half of the target item. If it's been dropped on the upper half, we'll insert before the target item, otherwise after the target item.

Finding out where exactly the mouse has been released is being accomplished with the help of ListViewItem.GetBounds(). This method allows you to query the rectangle the ListViewItem occupies. I've altered the MouseUp event handler accordingly:

C#
// use 0 instead of e.X so that you don't have
// to keep inside the columns while dragging
ListViewItem itemOver = listView1.GetItemAt(0, e.Y);
 
if (itemOver == null)
    return;
 
Rectangle rc = itemOver.GetBounds(ItemBoundsPortion.Entire);
 
// find out if we insert before or after the item the mouse is over
bool insertBefore;
if (e.Y < rc.Top + (rc.Height / 2))
    insertBefore = true;
else
    insertBefore = false;
 
if (_itemDnD != itemOver)
// if we dropped the item on itself, nothing is to be done
{
    if (insertBefore)
    {
        listView1.Items.Remove(_itemDnD);
        listView1.Items.Insert(itemOver.Index, _itemDnD);
    }
    else
    {
        listView1.Items.Remove(_itemDnD);
        listView1.Items.Insert(itemOver.Index + 1, _itemDnD);
    }
}

Please note that I also checked whether the item is being dropped on itself. In this case, we don't have to do anything. With this modification, we can place the item before the first or after the last ListViewItem in the ListView.

Giving Feedback by Extending the ListView

Now that we can decide where exactly to insert the item, a little more feedback would be nice, don't you think? I was thinking of drawing a colored line where the item will be inserted if you release the button. But, how to achieve this goal? We can find out whether to insert before or after the current item in the MouseMove event, just like we already do in MouseUp, so that's not the problem.

What is a little complicated, though, is how to perform the actual drawing onto the ListView.

Usually, to perform some extra painting on a control, you can derive from the control, override OnPaint(), call the OnPaint() of your base class, and then perform your own additional painting.

Unfortunately, the ListView doesn't call OnPaint() (or OnPaintBackground()), because it's still just a wrapper around the CommonControls ListView (has been this way since .NET 1.0).

Nevertheless, there is a way to accomplish custom painting on the ListView. Every control has a protected method WndProc() in which you can get access to all the Windows messages the control receives. This is a very powerful method; you're right at the pulse of the control, so to speak. Mess it up and your control is dead (doesn't work as expected or work at all); play it right and you can do almost anything with the control. Since OnPaint() usually is being called when the WM_PAINT message is received, we can catch this message here and perform our painting.

The overridden method in a ListView derived class looks like this:

C#
protected override void WndProc(ref Message m)
{
    base.WndProc(ref m);
 
    if (m.Msg == WM_PAINT)
    {
        if (LineBefore >= 0 && LineBefore < Items.Count)
        {
            Rectangle rc = Items[LineBefore].GetBounds(ItemBoundsPortion.Entire);
            DrawInsertionLine(rc.Left, rc.Right, rc.Top);
        }
 
        if (LineAfter >= 0 && LineBefore < Items.Count)
        {
            Rectangle rc = Items[LineAfter].GetBounds(ItemBoundsPortion.Entire);
            DrawInsertionLine(rc.Left, rc.Right, rc.Bottom);
        }
    }
}

WM_PAINT, in this case, is a constant I defined with the corresponding value (0x000f) taken from one of the Windows header files (WinUser.h). You can find them in a subdirectory of your Visual Studio installation, for example.

LineBefore and LineAfter are two new properties I added to the new ListView. They can be set to an item index to make the ListView draw an insertion line before or after this item.

The painting of the insertion line has been encapsulated in a private method DrawInsertionLine(), if you wonder where this comes from.

Now that the ListView actually can display insertion lines, the rest is just a matter of setting the right properties and telling the ListView to repaint itself (so that a WM_PAINT is sent and the painting is performed) by calling its Invalidate() method.

Summary

I hope I could show you how drag and drop operations inside a ListView can be accomplished with a few simple steps.

The basic task isn't very hard, but as soon as you want to implement a little more sophisticated control, you should know a little bit about message handling in .NET (and Windows) controls.

History

  • 17th June, 2006: Version 1.0 - Initial release

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Software Developer (Senior) 4voice AG
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionbug fix Pin
Elior Nasich24-Sep-18 23:52
Elior Nasich24-Sep-18 23:52 
QuestionGood work, but ... Pin
Jeremy Bradshaw9-Apr-17 12:00
Jeremy Bradshaw9-Apr-17 12:00 
QuestionFailed in Multiple Select ?.. Pin
Member 1036751016-Oct-16 5:17
Member 1036751016-Oct-16 5:17 
GeneralThank you very much! Pin
Codacity14-Sep-14 4:46
Codacity14-Sep-14 4:46 
QuestionManual reordering of items inside a ListView and update to sql database Pin
sergio09058816-Apr-14 7:32
sergio09058816-Apr-14 7:32 
QuestionHow can this be done in WPF? Pin
sergejg6-Nov-10 14:28
sergejg6-Nov-10 14:28 
GeneralI am not able to download source code Pin
knajay30-Nov-09 6:22
knajay30-Nov-09 6:22 
GeneralGood solution Pin
Cyril Gupta9-Jan-08 19:54
Cyril Gupta9-Jan-08 19:54 
QuestionMove multiple items Pin
Indra PR6-Jan-08 9:16
Indra PR6-Jan-08 9:16 
GeneralGreat! Pin
LhugS26-Jun-07 2:18
LhugS26-Jun-07 2:18 
GeneralRe: Great! Pin
mav.northwind26-Jun-07 8:24
mav.northwind26-Jun-07 8:24 
GeneralVery Nice!!! Pin
james3370718-Apr-07 2:42
james3370718-Apr-07 2:42 
GeneralLarge Icon view doesn't work Pin
Donald Man20-Jun-06 17:23
Donald Man20-Jun-06 17:23 
GeneralRe: Large Icon view doesn't work Pin
mav.northwind20-Jun-06 23:49
mav.northwind20-Jun-06 23:49 
GeneralRe: Large Icon view doesn't work Pin
edge942131-May-09 0:16
edge942131-May-09 0:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.