Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Drag and Drop Items in a WPF ListView

0.00/5 (No votes)
13 Apr 2007 21  
Discusses automated drag-and-drop in the WPF ListView.

Sample Image - ListViewDragDropManager.png

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 ListViews.
  • 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 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 - Gets/sets 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 - Gets/sets 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 - Gets/sets 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 ListViewItems appropriately.

<Style x:Key="ItemContStyle" TargetType="ListViewItem">
  <!-- These triggers react to changes in the attached properties set
       during a managed drag-drop operation. -->
  <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 )
{
 // This shows how to customize the behavior of a drop.

 // Here we perform a swap, instead of just moving the dropped item.


 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 ListViews.

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 )
{
 // We need to use MouseUtilities to figure out the cursor

 // coordinates because, during a drag-drop operation, the WPF

 // mechanisms for getting the coordinates behave strangely.


 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 );

  // In case the cursor is at the very top or bottom of the ListViewItem

  // we want to make the vertical threshold very small so that dragging

  // over an adjacent item does not select it.

  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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here