Introduction
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.
Background
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
ListView
. - Support for drag-and-drop operations between two
ListView
s. - 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
ListViewItem
s at the same time. - No guarantees that it will work if you set the
ListView
's View
to a custom view implementation. I have only tested the code when using the standard GridView
as the ListView
's View
. - No support for type conversions when attempting to drop an item of a type different than the
ListView
's items. - The
ListView
's ItemsSource
must reference an ObservableCollection<ItemType>
, where ItemType
corresponds to the type parameter of ListViewDragManager<ItemType>
. - 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
PInvokes
into 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
ListView
's ItemsSource
collection. 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 ListView
.
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 Window
's 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 ListView
's ItemsSource
property must reference an ObservableCollection<Foo>
. Alternatively the ItemsSource
property could be bound to the DataContext
property, and have the latter reference an ObservableCollection<Foo>
.
Before going any further into how to use the ListViewDragDropManager
, let's take a look at its public properties:
DragAdornerOpacity
- Get
s/set
s the opacity of the drag adorner. This property has no effect if ShowDragAdorner
is false
. The default value is 0.7
IsDragInProgress
- Returns true
if there is currently a drag operation being managed. ListView
- Get
s/set
s the ListView
whose dragging is managed. This property can be set to null
, to prevent drag management from occurring. If the ListView
's AllowDrop
property is false
, it will be set to true
. ShowDragAdorner
- Get
s/set
s whether a visual representation of the ListViewItem
being dragged follows the mouse cursor during a drag operation. The default value is true
.
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
When a 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 ListView
's ItemContainerStyle
, which uses the aforementioned attached properties to style the ListViewItem
s appropriately.
<Style x:Key="ItemContStyle" TargetType="ListViewItem">
<Style.Triggers>
<Trigger Property="jas:ListViewItemDragState.IsBeingDragged" Value="True">
<Setter Property="FontWeight" Value="DemiBold" />
</Trigger>
<Trigger Property="jas:ListViewItemDragState.IsUnderDragCursor" Value="True">
<Setter Property="Background" Value="Blue" />
</Trigger>
</Style.Triggers>
</Style>
That 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 ListView
s.
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.
Cursor location
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.
ListViewItem index
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 IndexUnderDragCursor
{
get
{
int index = -1;
for( int i = 0; i < this.listView.Items.Count; ++i )
{
ListViewItem item = this.GetListViewItem( i );
if( this.IsMouseOver( item ) )
{
index = i;
break;
}
}
return index;
}
}
ListViewItem GetListViewItem( int index )
{
if( this.listView.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated )
return null;
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:
bool HasCursorLeftDragThreshold
{
get
{
if( this.indexToSelect < 0 )
return false;
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 );
}
}
Revision History
- 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.