This article presents a class called
ListViewDragDropManager, which automates drag-and-drop operations in the WPF
ListView. It allows the user to drag and drop items within a
ListView, or drag items from one
ListView to another. The class is smart enough to figure out where in the
ListView the user wants to drop an item, and will insert the item at that location automatically. The class also exposes several properties and one event that enables a developer to customize the way that the drag-and-drop operations behave.
Drag-and-drop is ubiquitous in modern user interfaces. Most users expect the simplicity and interactivity which drag-and-drop provides to be present in any application they use frequently. It makes life easier for the user.
WPF has support for drag-and-drop built into its framework, but there is still a lot of legwork you must take care of to make the out-of-the-box functionality work smoothly. I decided to create a class which would take care of that legwork, so that any applications I write that require drag-and-drop in a
ListView can get it "for free."
I thought that implementing such a class would be a trivial effort. I was wrong. The basic functionality was easy enough to encapsulate in a helper class, but then many gotchas and what-ifs cropped up after the core functionality was in place. I decided that nobody else should have to wade through that swamp of frustration again, so I posted the finished product here on CodeProject. Now you can get drag-and-drop in a WPF
ListView with just one line of code!
What it is
WPF is very flexible, and that can make it difficult to create a generic helper class which provides a simple service.
ListViewDragDropManager does not solve every possible problem related to drag-and-drop in a
ListView, but it should be sufficient for most scenarios. Let's take a moment to review what it provides:
- Automated drag-and-drop operations for items within the same
- Support for drag-and-drop operations between two
- An optional drag adorner - a visual representation of the
ListViewItem being dragged which follows the mouse cursor (see screenshot above).
- The ability to modify the opacity (translucency) of the drag adorner.
- A means of alerting a
ListViewItem of when it is being dragged, which can be used for styling purposes.
- A means of alerting a
ListViewItem of when it is under the drag cursor, which can be used for styling purposes.
- An event which fires when an item has been dropped, allowing you to run custom logic for relocating the item which was dropped.
- Mouse interaction with controls in a
ListViewItem, such as a
CheckBox that works properly (that was "not" easy to get right!).
What it is not
As mentioned previously, the
ListViewDragDropManager does not cover all the bases. Here are some features I left out (at least for the time being):
- No support for drag-and-drop of multiple
ListViewItems at the same time.
- No guarantees that it will work if you set the
View to a custom view implementation. I have only tested the code when using the standard
GridView as the
- No support for type conversions when attempting to drop an item of a type different than the
ItemsSource must reference an
ItemType corresponds to the type parameter of
- The drag adorner will not leave the
ListView in which it was created.
- The class cannot be easily used in XAML, because it has a generic type parameter.
- It cannot be used in an application which is not granted full trust permissions, because it
User32 to get the mouse cursor location. This means that the class is probably not safe to use in a Web browser application (XBAP) unless it is explicitly granted full trust permissions.
- Limited support for null values in the
ObservableCollection throws exceptions when you try to move or remove null values (I don't know why, but it does). I doubt this will be an issue for too many people, because null items render blank and are not very useful when put in a
Using the code
As promised earlier, the
ListViewDragDropManager allows you to have full-featured drag-and-drop in a
ListView with just one line of code. Here's that one line:
new ListViewDragDropManager<Foo>( this.listView );
There are a few things to point out about that one line of code. You might want to put it in a
Loaded event handling method, so that the
ListView has drag-and-drop support as soon as the
Window opens. The '
Foo' type parameter indicates what type of objects the
ListView is displaying. The
ItemsSource property must reference an
ObservableCollection<Foo>. Alternatively the
ItemsSource property could be bound to the
DataContext property, and have the latter reference an
Before going any further into how to use the
ListViewDragDropManager, let's take a look at its public properties:
sets the opacity of the drag adorner. This property has no effect if
false. The default value is
IsDragInProgress - Returns
true if there is currently a drag operation being managed.
ListView whose dragging is managed. This property can be set to
null, to prevent drag management from occurring. If the
AllowDrop property is
false, it will be set to
sets whether a visual representation of the
ListViewItem being dragged follows the mouse cursor during a drag operation. The default value is
There is also one event exposed:
ProcessDrop - Raised when a drop occurs. By default the dropped item will be moved to the target index. Handle this event if relocating the dropped item requires custom behavior. Note, if this event is handled the default item dropping logic will not occur.
Styling the items
ListViewItem is being dragged you might want to style it differently than the other items. You might also want to style the
ListViewItem under the drag cursor – not necessarily the item being dragged, but whatever item the cursor is currently over. To do that, you can make use of the attached properties exposed by the static
ListViewItemDragState class. Here is a small example of a
Style used by a
ItemContainerStyle, which uses the aforementioned attached properties to style the
<Style x:Key="ItemContStyle" TargetType="ListViewItem">
<!-- These triggers react to changes in the attached properties set
during a managed drag-drop operation. -->
<Trigger Property="jas:ListViewItemDragState.IsBeingDragged" Value="True">
<Setter Property="FontWeight" Value="DemiBold" />
<Trigger Property="jas:ListViewItemDragState.IsUnderDragCursor" Value="True">
<Setter Property="Background" Value="Blue" />
Style will make a
ListViewItem have demi-bold text when it is being dragged, or have a blue background when the drag cursor is over it.
Custom drop logic
By default when an item is dropped in a
ListView managed by the
ListViewDragDropManager the item is moved from its current index to the index which corresponds to the location of the mouse cursor. It is possible that different item relocation logic might be required for some applications. To accommodate those situations, the
ListViewDragDropManager raises the
ProcessDrop event when a drop occurs. If you handle that event, you must move the dropped item into its new location, the default relocation logic will not execute.
One example of custom drop logic might be to "swap" the item which is being dropped with the item that occupies the target index. For example, suppose the
ListView has three items in this order: 'A', 'B', 'C'. Also imagine that the user drags item 'A' and drops it over item 'C'. The default drop logic will result in the items being in this order: 'B', 'C', 'A'. However, with the "swap" logic in place, we would expect the items to be in this order after the drop finishes: 'C', 'B', 'A'.
Below is an implementation of the "swap" logic:
void OnProcessDrop( object sender, ProcessDropEventArgs<Foo> e )
int higherIdx = Math.Max( e.OldIndex, e.NewIndex );
int lowerIdx = Math.Min( e.OldIndex, e.NewIndex );
e.ItemsSource.Move( lowerIdx, higherIdx );
e.ItemsSource.Move( higherIdx - 1, lowerIdx );
e.Effects = DragDropEffects.Move;
The source code download at the top of this article has a demo application, which shows how to use all of the features seen above. It also demonstrates how to implement drag-and-drop between two
Tips and tricks
I am not going to bother showing how the code works, because it is relatively complicated and would require dozens of pages of code and explanation to convey the general gist. Instead we will examine some of the code which took me a long time to figure out and get right.
The biggest problem I faced was getting the mouse cursor coordinates during a drag-drop operation. The WPF mechanisms for getting the cursor location fall apart during drag-and-drop. To circumvent this issue, I call into unmanaged code to get the cursor location. Dan Crevier, a member of the WPF group at Microsoft, seems to have faced the same problem and posted a workaround here. Once I started using his
MouseUtilities class, all of my cursor woes went away.
Another tricky piece of the puzzle was figuring out the index of the
ListViewItem under the mouse cursor. The
ListViewDragDropManager needs this information in order to know which item the user is trying to drag, where to move a dropped item to, and to let the
ListViewItemDragState class know when to indicate that the cursor is over a
ListViewItem. My implementation is shown below:
int index = -1;
for( int i = 0; i < this.listView.Items.Count; ++i )
ListViewItem item = this.GetListViewItem( i );
if( this.IsMouseOver( item ) )
index = i;
ListViewItem GetListViewItem( int index )
if( this.listView.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated )
return this.listView.ItemContainerGenerator.ContainerFromIndex( index ) as ListViewItem;
bool IsMouseOver( Visual target )
Rect bounds = VisualTreeHelper.GetDescendantBounds( target );
Point mousePos = MouseUtilities.GetMousePosition( target );
return bounds.Contains( mousePos );
Drag distance threshold
The last tricky piece of code I'm going to show here determines when the drag operation should begin. In Windows there is the concept of a "drag distance threshold", which specifies how far the mouse must move after the left mouse button is pressed, before a drag-and-drop operation may begin.
ListViewDragDropManager attempts to honor that threshold, but in some scenarios must decrease the vertical threshold value. This is because if the cursor is very near the top or bottom edge of a
ListViewItem when the left mouse button is pressed, the
ListView will select the neighboring
ListViewItem when the cursor moves over it. To prevent that from happening, it will decrease the vertical threshold if the cursor is very near the top or bottom edge of the
ListViewItem to be dragged. Here's how that works:
if( this.indexToSelect < 0 )
ListViewItem item = this.GetListViewItem( this.indexToSelect );
Rect bounds = VisualTreeHelper.GetDescendantBounds( item );
Point ptInItem = this.listView.TranslatePoint( this.ptMouseDown, item );
double topOffset = Math.Abs( ptInItem.Y );
double btmOffset = Math.Abs( bounds.Height - ptInItem.Y );
double vertOffset = Math.Min( topOffset, btmOffset );
double width = SystemParameters.MinimumHorizontalDragDistance * 2;
double height = Math.Min(
SystemParameters.MinimumVerticalDragDistance, vertOffset ) * 2;
Size szThreshold = new Size( width, height );
Rect rect = new Rect( this.ptMouseDown, szThreshold );
rect.Offset( szThreshold.Width / -2, szThreshold.Height / -2 );
Point ptInListView = MouseUtilities.GetMousePosition( this.listView );
return !rect.Contains( ptInListView );
- February 1, 2007 - Fixed two bugs. One of them was pointed out by micblues, regarding drag-drop operation being inappropriately begun when dragging the
ListView's scrollbar. The other bug had to do with an incorrect drag adorner location when the
ListView was scrolled to the right. The updated source code was posted as well.
- February 25, 2007 - Fixed a bug in the
MouseUtilities class which only occurred on a machine using a higher screen resolution than the standard 96 DPI. The bug was reported and resolved by William J. Roberts (aka Billr17) via this article's messageboard. The updated source code was also posted.
- April 13, 2007 - Fixed a minor issue with the positioning of the drag adorner. The adorner used to "snap" into position, such that the top of mouse cursor and the top of the adorner would intersect when the drag began. I fixed it so that the top of the mouse cursor would stay in the same position within the adorner (relative to where the cursor was within the dragged
ListViewItem). The updated source code was posted.