How to drag and drop items between data bound ItemsControls

In this blog post, I will explain how I implemented a sample that allows you to drag and drop items between data bound ItemsControls. Imagine you have two or more data bound (or not data bound) ItemsControls in the same Window. You may want to drag an item from an ItemsControl and drop it on another one. Or maybe you want to drag and drop it in a different position within the same ItemsControl. The code in this blog post allows you to do those operations.
Use this code
You don’t need to understand the implementation details of this sample to take advantage of its functionality. If you want to use this code in your app, you should copy three files from the project at the end of the post - DraggedAdorner.cs, InsertionAdorner.cs and DragDropHelper.cs. You may want to change the namespace in these files. Then, within your app, you simply need to add three attached properties:
- IsDropTarget should be set on the ItemsControl that you want to drag items to. This property is of type boolean and should be set to true. Don’t forget to add a namespace mapping on the top window that maps to the namespace you chose for DragDropHelper.
<Window …
xmlns:local="clr-namespace:DragDropListBox" … />
<ItemsControl …
local:DragDropHelper.IsDropTarget="true" …/>
- IsDragSource should be set on the ItemsControl that you want to drag items from. This property should also be set to true.
- DragDropTemplate should also be set on the drag source ItemsControl (where you want to drag the items from). This property should be set to a DataTemplate that controls how the data item should be displayed while it is dragged around. If you want it to be displayed exactly the same way as in the source ItemsControl, the DragDropTemplate and ItemTemplate properties can share the same template.
<ItemsControl …
local:DragDropHelper.IsDragSource="true" local:DragDropHelper.DragDropTemplate="{StaticResource pictureTemplate}"/>
And that’s all there is to it. Next I will show the list of features and limitations of this solution.
Features
- The code in this sample enables the user to drag and drop between ItemsControls and any controls that derive from it (see exception for TreeView in the limitations section).
- This solution supports dragging and dropping within the same ItemsControl.
- This solution supports dragging and dropping between non data bound and data bound ItemsControls.
- I’ve added support for ItemsControls that use FlowPanel and StackPanel (with both horizontal and vertical orientation). Support for other panels can be added easily.
- This sample prevents the user from dropping a data item into a target ItemsControl when its data items are of an incompatible type. For example, if the drag source ItemsControl contains a collection of Potatoes but the drop target ItemsControl is data bound to a collection of Squirrels, the app will prevent the user from doing the drop.
- This sample provides visual feedback of where the dragged item will go if dropped. For example, when dragging a data item over a ListBox, this sample shows a horizontal line between the two items where the item will go if dropped.
Limitations
- This solution doesn’t drag and drop data items across Windows.
- It also doesn’t work across Applications.
- It allows drag and drop to work between ItemsControls, but it does not allow you to drag or drop to a ContentControl.
- This sample works as expected for the first level of a TreeView, but not for nested levels of the hierarchy.
Overview
To implement a drag and drop operation, we need to attach handlers for several events on the drag source and drop target elements. In this scenario, I decided that I am going to support only ItemsControls as the source and target of the drag and drop operation. All the events that I need to provide handlers for are public, so I could have easily done that in the Window1.xaml.cs file. However, I wanted an easily reusable solution that would allow all the drag-and-drop-specific code to be abstracted in one place. I thought of three different solutions to this problem (although I’m sure there are more):
- I could derive from ItemsControl and override “OnPreviewMouseLeftButtonDown” and all the other “On” methods that correspond to the events I care about. I don’t like this solution so much because users would have to replace their ItemsControls with my custom “DragAndDropItemsControl”.
- I could create a DragHelper class that takes an instance of the drag source ItemsControl as a parameter, and a DropHelper class that takes the drop target. The only disadvantage of this solution is that it needs to be set up from code.
- Instead, I decided to create a DragDropHelper class that defines properties to be attached to the drag source and drop target - IsDragSource and IsDropTarget. The main advantages of this solution are that it can be implemented entirely in XAML, and that the XAML changes are minimal (you only need to set these properties). I’ll explain below how this works.
These two attached properties are of type boolean, and should be attached to the drag source and drop target. When they’re set to false, dragging/dropping is not allowed, otherwise it is. If your application requires drag and drop operations to be allowed sometimes but not other times, you can simply change the properties on the fly.
When registering the attached properties, I made sure I passed change handlers called IsDragSourceChanged and IsDropTargetChanged to the UIPropertyMetadata. These methods are responsible for adding and removing handlers for the desired events, depending on the value of the attached property. I will show here the code for IsDragSourceChanged (the code for the target is similar):
private static void IsDragSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var dragSource = obj as ItemsControl;
if (dragSource != null)
{
if (Object.Equals(e.NewValue, true))
{
dragSource.PreviewMouseLeftButtonDown += Instance.DragSource_PreviewMouseLeftButtonDown;
dragSource.PreviewMouseLeftButtonUp += Instance.DragSource_PreviewMouseLeftButtonUp;
dragSource.PreviewMouseMove += Instance.DragSource_PreviewMouseMove;
}
else
{
dragSource.PreviewMouseLeftButtonDown -= Instance.DragSource_PreviewMouseLeftButtonDown;
dragSource.PreviewMouseLeftButtonUp -= Instance.DragSource_PreviewMouseLeftButtonUp;
dragSource.PreviewMouseMove -= Instance.DragSource_PreviewMouseMove;
}
}
}
I describe below the events that I provided handlers for. I had to use the preview versions of these events to make sure I handle them before other controls do (for example, ListBox handles MouseLeftButtonDown).
ItemsControl Drag Source
- PreviewMouseLeftButtonDown - Its main responsibility is to remember the data item that the user clicked on.
this.sourceItemContainer = Utilities.GetItemContainer(this.sourceItemsControl, visual);
if (this.sourceItemContainer != null)
{
this.draggedData = this.sourceItemContainer.DataContext;
}
- PreviewMouseMove - For a drag to happen, the user needs to click on the drag source and move the mouse by a significant amount. When a user performs a button click, there is a pretty high likelihood that the mouse will move inadvertently by a small amount, and I want to ignore those situations. Fortunately, there is a SystemParameter that can be used in these situations, so I make sure that the mouse has moved by at least SystemParameters.MinimumHorizontalDragDistance or SystemParameters.MinimumVerticalDragDistance before I consider it a drag. Once I decide a drag has started, I initiate the drag-and-drop operation. Notice below that I am initiating the drag operation with the data that I determined in the PreviewMouseLeftButtonDown handler (draggedData).
DataObject data = new DataObject(this.format.Name, this.draggedData);
…
DragDropEffects effects = DragDrop.DoDragDrop((DependencyObject)sender, data, DragDropEffects.Move);
- PreviewMouseLeftButtonUp - This happens when the user releases the mouse without initiating a drag-and-drop operation. In this case, I simply set the draggedData field to null.
this.draggedData = null;
ItemsControl Drop Target
- PreviewDragEnter - This event fires every time the user first drags a data item into an element with AllowsDrop=true (I set this property on the drop target ItemsControl within the IsDropTargetChanged method, so you don’t have to). Since this property is inherited, this handler will be called every time the mouse enters any element that belongs to the visual tree of the drop target ItemsControl. Among other things, within this handler I have code that updates the two adorners that I will explain later in this post.
- PreviewDragOver - This event fires when the user drags the data item over elements with AllowsDrop=true. Similarly to the previous handler, the adorners are updated in this handler.
- PreviewDragLeave - As expected, this event fires when the mouse leaves an element that has AllowsDrop=true. Here, I remove one of the adorners. More details on the adorners later.
- PreviewDrop - This event notifies us that the drop operation has actually occurred. Here, I remove the data item from the drag source ItemsControl’s collection and add it to the drop target.
object movedItem = e.Data.GetData(this.format.Name);
…
indexRemoved = Utilities.RemoveItemFromItemsControl(this.sourceItemsControl, movedItem);
…
Utilities.InsertItemInItemsControl(this.targetItemsControl, movedItem, this.insertionIndex);
Dragging and dropping data
There are several problems that are particular to dragging and dropping data items between ItemsControls. I will discuss a few of those next.
When dragging a data item over the target ItemsControl, I need to determine where the item will go if dropped. I broke down this scenario into three possible different situations:
- The mouse is over an empty ItemsControl. In this case, if the ItemsControl is data bound to an empty collection the item gets added as the first and only item of that collection; similarly, if the ItemsControl is empty and not data bound, the item is added to the Items collection of the ItemsControl.
- The ItemsControl has items, although the mouse is over the empty part of the ItemsControl, after the last item. In this case, the item should be added to the end of the ItemsSource or Items collection.
- Last, the mouse could be over an item container. In this case, I need to determine whether I should insert the dragged data before or after the item under the mouse. If the ItemsControl uses a panel with vertical orientation, I check to see whether the mouse is over the top half or bottom half of the item container. Similarly, if the panel is laid out horizontally, I check to see whether the mouse is over the left half or right half of the item container.
This leads me to the next topic: I had to store the orientation of the panel so that I could calculate the insertion location when dropping over an item container. This calculation is done in the following method:
public static bool HasVerticalOrientation(FrameworkElement itemContainer)
{
bool hasVerticalOrientation = true;
if (itemContainer != null)
{
Panel panel = VisualTreeHelper.GetParent(itemContainer) as Panel;
StackPanel stackPanel;
WrapPanel wrapPanel;
if ((stackPanel = panel as StackPanel) != null)
{
hasVerticalOrientation = (stackPanel.Orientation == Orientation.Vertical);
}
else if ((wrapPanel = panel as WrapPanel) != null)
{
hasVerticalOrientation = (wrapPanel.Orientation == Orientation.Vertical);
}
// You can add support for more panel types here.
}
return hasVerticalOrientation;
}
Notice that I am providing support only for StackPanel and WrapPanel here, but you can easily add more “if” statements to support other panels.
Once I have the panel’s orientation information, I can determine whether the dragged data should be inserted before or after the item container under the mouse. In the code below, “first half” means the left half for a horizontally oriented panel, and top half for a vertically oriented one.
public static bool IsInFirstHalf(FrameworkElement container, Point clickedPoint, bool hasVerticalOrientation)
{
if (hasVerticalOrientation)
{
return clickedPoint.Y < container.ActualHeight / 2;
}
return clickedPoint.X < container.ActualWidth / 2;
}
Finally, I wanted to give the user feedback when a certain data item type can not be added to the drop target collection. To achieve this, I wrote a method called IsDropDataTypeAllowed, from which the following code snippet was taken. The drop operation is allowed if the target collection is of type IList
Type draggedType = draggedItem.GetType();
Type collectionType = collectionSource.GetType();
Type genericIListType = collectionType.GetInterface("IList`1");
if (genericIListType != null)
{
Type[] genericArguments = genericIListType.GetGenericArguments();
isDropDataTypeAllowed = genericArguments[0].IsAssignableFrom(draggedType);
}
else if (typeof(IList).IsAssignableFrom(collectionType))
{
isDropDataTypeAllowed = true;
}
else
{
isDropDataTypeAllowed = false;
}
Dragged Adorner
As I mentioned earlier, the DragDropHelper class contains a DragDropTemplate attached property that should be set to the DataTemplate responsible for determining the look of the dragged data item. The DraggedAdorner’s main responsibility is to provide a representation for the dragged data using that DataTemplate. To achieve this behavior, I decided that the adorner’s only child should be a ContentPresenter with its Content set to the dragged data, and its ContentTemplate set to the DataTemplate specified in the DragDropTemplate attached property. I also decided to give the adorner a bit of transparency, so I set the Opacity of the ContentPresenter to 0.7. Here is the constructor for this adorner:
public DraggedAdorner(object dragDropData, DataTemplate dragDropTemplate, UIElement adornedElement, AdornerLayer adornerLayer)
: base(adornedElement)
{
this.adornerLayer = adornerLayer;
this.contentPresenter = new ContentPresenter();
this.contentPresenter.Content = dragDropData;
this.contentPresenter.ContentTemplate = dragDropTemplate;
this.contentPresenter.Opacity = 0.7;
this.adornerLayer.Add(this);
}
And here is what this adorner looks like, with the template I specified in this blog’s project:
There’s a bit more logic in this adorner class, such as the code that allows the adorner to follow the position of the mouse and the code that sets up the ContentPresenter as the adorner’s child visual.
Next I will explain briefly when this adorner is created, removed, and updated.
The DraggedAdorner is created:
- When the user first drags data into an element belonging to the drop target - DropTarget_PreviewDragEnter. This event is fired every time we enter a new element in the target’s tree, but we only need to create the Dragged Adorner the first time.
- Every time we re-enter the Window - TopWindow_DragEnter.
The DraggedAdorner is removed:
- When dropping the item in a valid area - DropTarget_PreviewDrop.
- When leaving the window - TopWindow_DragLeave.
- If I drop the item in an invalid area, the drop handler doesn’t get called but the Window’s leave handler does, so this scenario is covered.
The DraggedAdorner is updated:
- Every time the cursor moves over the drop target or anywhere else within the window. - DropTarget_PreviewDragOver, TopWindow_DragOver.
In summary, I create the adorner once, the first time it is needed. I remove the adorner every time the mouse leaves the window and recreate it when the mouse re-enters the window. And I remove the adorner again at the end of the drag-and-drop operation.
Insertion Adorner
The insertion adorner is responsible for the visual feedback that indicates the insertion point of a dropped data item. For example, if you drag a data item over a ListBox, you will see a line between two of the items indicating the position where the dragged item will be inserted if dropped. The visual appearance of an insertion adorner is a line with two triangles facing inwards at the ends:
In order to make this adorner as performant as possible, I wrote a static constructor where I create a triangle and freeze all Freezable objects involved in its creation:
static InsertionAdorner()
{
pen = new Pen { Brush = Brushes.Gray, Thickness = 2 };
pen.Freeze();
LineSegment firstLine = new LineSegment(new Point(0, -5), false);
firstLine.Freeze();
LineSegment secondLine = new LineSegment(new Point(0, 5), false);
secondLine.Freeze();
PathFigure figure = new PathFigure { StartPoint = new Point(5, 0) };
figure.Segments.Add(firstLine);
figure.Segments.Add(secondLine);
figure.Freeze();
triangle = new PathGeometry();
triangle.Figures.Add(figure);
triangle.Freeze();
}
I need two triangles in this adorner. Because their positions and orientations vary according to the ItemsControl’s layout, I created just one triangle object and draw it twice after applying the appropriate translate and rotate transforms. The method below is called twice with different parameters:
private void DrawTriangle(DrawingContext drawingContext, Point origin, double angle)
{
drawingContext.PushTransform(new TranslateTransform(origin.X, origin.Y));
drawingContext.PushTransform(new RotateTransform(angle));
drawingContext.DrawGeometry(pen.Brush, null, triangle);
drawingContext.Pop();
drawingContext.Pop();
}
Next I will explain briefly when this adorner is created, removed, and updated.
The InsertionAdorner is created:
- Every time the cursor enters a new element in the target’s tree. It needs to be created this frequently because different elements can have different sizes - DropTarget_PreviewDragEnter.
The InsertionAdorner is removed:
- Every time the cursor leaves the current element or the item is dropped - DropTarget_PreviewDragLeave, DropTarget_PreviewDrop.
The InsertionAdorner is updated:
- Every time the cursor moves over the drop target - DropTarget_PreviewDragOver.
Windows events
The fact that I’m providing handlers for the window’s drag drop events may seem a little strange at first.
Imagine I provide handlers for the ItemsControl drop target only. Since I have code in these handlers that displays the preview adorner (which is where DragDropTemplate gets applied), the preview would only come up when the mouse cursor enters the drop target. Anywhere else in the window, the mouse cursor would turn into a crossed circle, indicating that it’s not an allowed drop location.
I like the crossed circle to come up in those areas, but I would like the adorner to be displayed anywhere I drag the item within the window.
To achieve the effect I wanted, I set the AllowDrop property of the window to true, and hooked up handlers for its drag events. I used the preview versions of the drop events for the target, but not for the window. This way, when the cursor is within the drop target I get the drop target events (and set Handled to true), but when the cursor is anywhere else in the window, I get the window’s events instead. I used these events to ensure the creation and removal of the preview dragged adorner, and to make sure that the mouse cursor is set to the crossed circle.
Other similar solutions available
Other developers have blogged about similar drag-and-drop solutions in the past. I will link to some of those solutions here, although there are probably more that I haven’t come across.
Pavan Podila has a great series of four blog posts about a similar scenario, which I highly recommend reading. The main difference between our two solutions is that his code can be used to drag UI elements from one panel to another, while my solution is used to drag data from one ItemsControl to another. I was very glad to see that Pavan Podila is also a fan of the attached properties solution for extending functionality.
Jaime Rodriguez also talked about this topic in a great three part series blog post. As in this post, he provides a solution for dragging and dropping data items.
If you need drag-and-drop functionality in your app, you will probably end up using a combination of the features from many blog posts. I hope you find my solution useful!
Here you can find the project with this code built using VS 2008 RTM.
Update March 3, 2010: I’ve updated the code with a bug fix. Previously, the location where the dragged adorned showed up depended on where the user clicked within the element. Now the dragged adorner always shows up immediately to the bottom right of the mouse. To do this, I first captured the mouse offset from the top left of the source item container, which I do in DragSource_PreviewMouseMove:
this.initialMouseOffset = this.initialMousePosition - this.sourceItemContainer.TranslatePoint(new Point(0, 0), this.topWindow);
Then I used this value when setting the position within ShowDraggedAdorner:
this.draggedAdorner.SetPosition(currentPosition.X - this.initialMousePosition.X + this.initialMouseOffset.X, currentPosition.Y - this.initialMousePosition.Y + this.initialMouseOffset.Y);
Paul Prewett
Hi Bea -
Nice work. Very helpful.
I’m trying hard to get my head around WPF and to that end, I have a question. My app needs to know after an item has been dropped and so to implement that, I created a new RoutedEvent (ItemDroppedEvent) and raised it manually before your code removed the adorners.
…
Utilities.InsertItemInItemsControl(this.targetItemsControl, draggedItem, this.insertionIndex);
RoutedEventArgs eventArgs = new RoutedEventArgs(DragDropHelper.ItemDroppedEvent);
((UIElement)sender).RaiseEvent(eventArgs);
RemoveDraggedAdorner();
…
Those two middle lines are my addition. Is this the best (i.e. proper, canonical, etc.) way to implement this type of feature or would you suggest an alternate approach?
Thanks very much.
-Paul Prewett
June 24, 2009 at 1:45 pm
Bea
Hi Paul,
Your code looks great.
I would maybe raise the event after removing the adorner, but it’s a minor detail.
Bea
November 20, 2009 at 2:07 pm
Visu
Bea,
Any suggestions on how to drag and drop multiple items from one TreeView into another?
July 10, 2009 at 4:41 am
Bea
Hi Visu,
Take a look at Groky’s code, which extends this post’s code to include drag-and-drop across TreeViews and supports multiple selection.
Bea
November 20, 2009 at 12:44 pm
bloonsterific
Just wanted to tell you all know how much I appreciate your postings guys.
Found you though google!
July 10, 2009 at 4:57 am
vallete
Great post. And I would definitely concur that the other posts that you quote are great for drag and drop.
However, have you been able to find a way to auto scroll the list box if you have too many items to display in the list box and you want the list to auto scroll in the a direction depending on whether the cursor is near the edge of the list box during a dragging operation.
July 20, 2009 at 2:54 pm
Bea
Hi Vallete,
I agree that that is a very important feature when you’re using drag and drop. I don’t have a solution implemented - that’s a good idea for a future blog post.
If you’ve implemented it, please let me know so that others can benefit from your code.
Thanks,
Bea
November 20, 2009 at 12:24 pm
Nathaniel
Very awesome, and useful stuff! worked great for my application, I threw in one feature that allows the targeted control to reject the dragged data using an Attached Event. If you are interested in the code let me know, didn’t want to dirty your page unnecessarily.
August 11, 2009 at 2:36 pm
Bea
Hi Nathaniel,
That’s a cool feature. If it’s not too much code, can you post it here? It may help others with the same problem. If it’s a lot of code send it to me through email - I can host it on my server and add a link to it.
Thanks!
Bea
October 11, 2009 at 1:15 pm
Michael White
I was so excited, then I heard the Awwwnnn in my head when I took a look at the code. I’m trying to do the same thing in Silverlight, and it is just kicking my but, This is fantastic code though, and I’ll probably reference it the next time I do a WPF project.
Thanks!
August 28, 2009 at 7:04 pm
serine
hi, i’ve analyzed your code and it is very helpful but i think this part:
Type collectionType = collectionSource.GetType();
Type genericIListType = collectionType.GetInterface(“IList`1″);
if (genericIListType != null)
rather should be:
Type collectionType = collectionSource.GetType();
Type genericIListType = collectionType.GetInterface(“IList`1″);
if (genericIListType != collectionType)
am i right?
September 3, 2009 at 9:42 am
Bea
Hi Serine,
“collectionType” can never be an interface (it will be a concrete type), and “genericIListType” is either null or an interface type. Therefore, the condition in your if statement is always true. I don’t think that’s what we want here.
Type genericIListType = collectionType.GetInterface(“IList`1″);
if (genericIListType != null)
{
Type[] genericArguments = genericIListType.GetGenericArguments();
isDropDataTypeAllowed = genericArguments[0].IsAssignableFrom(draggedType);
}
The goal of this code is to verify whether the collection implements IList<T>. If it does, I get the type of T and check whether the element that I’m dragging can be assigned to T. If it can’t, I don’t allow dropping.
Let me know if you have a scenario that doesn’t work with this code.
Bea
October 11, 2009 at 7:17 pm
Karthick
U r a Genius.. nice post…..
September 10, 2009 at 11:20 am
Chris A
Hi Bea, I’ll start by saying great code, simple to use, gotta love it, many thanks!
Now for my question…uh oh…
The scenario is this, I have a listview that shows some items in a gridview.
I want to be able to sort those items using drag and drop within the listview - job done - using your code! :o)
I also want to be able to double click an item and open a dialog for editing.
I’m trying to follow the MVVM pattern, again, your code fits perfectly here seeing as it’s wired up in XAML.
In order to follow the MVVM pattern I’ve also used an attached behaviour to wire up the mouse double click of the listview to an ICommand in my ViewModel. There are quite a few blog posts about this sort of thing, I claim no credit for it.
The finished thing kind of works, but there’s a small bug.
The bug seems to be:
1, You double click an item in the list.
2, The dialog opens for editing.
3, You OK or Cancel the dialog (it makes no difference).
4, If you now roll the mouse over the listview it thinks you’re still in drag and drop mode and it moves the item you double clicked on accordingly.
I’m guessing this is to do with something that gets kicked off on he left mouse button down event - is there an easy way to cancel that if the left mouse button is actually double clicked?
I’ve tried to be as thorough as I can - and I could be completely wrong in my assessment of the problem.
If you have time I’m happy to share the code, otherwise, suffice to say, it’s working right now as I’ve implemented the ‘Edit’ command on a separate button rather than on the double click of the listview! :o)
Thanks again, much appreciated…
CA.
October 21, 2009 at 9:38 am
Chris A
Bea,
I found a solution to a problem I was having with double clicking an item in the drag source.
I was using an attached behavior to execute a command on mousedoubleclick of the listview that was the dragsource yet the dragdrophelper was still intiating a drag.
I found that the order of events is as follows (which is probably documented somewhere)…
Single Click:
1, Preview Mouse Left Button Down
2, Preview Mouse Left Button Up
Double Click:
1, Preview Mouse Left Button Down
2, Preview Mouse Left Button Up
3, Preview Mouse Double Click
4, Preview Mouse Left Button Down
5, Mouse Double Click
I was planning to handle the Preview Mouse Double Click event and set draggedData to null (cancelling the drag operation).
However, as you can see from above, Preview Left Mouse Button Down fires again right after the Preview Mouse Double Click event.
Instead I now set draggedData to null in the Mouse Double Click event which effectively cancels the drag/drop operation if you double click an item in your dragsource.
Just thought I’d share that with you as the original author and anyone else who may be using your code and experiencing similar issues.
Many thanks,
CA
October 22, 2009 at 2:10 am
Bea
Hi Chris,
Thanks for posting the solution! (and sorry I didn’t have a chance to get to your question earlier…)
Bea
October 22, 2009 at 9:16 am
Groky
Thanks for posting this! I found it extemely useful, but I needed more ;). I took the ideas from your example, expanded on them, and I’ve created a project on Google Code to share it. My library has various extra features, including support for MVVM, TreeView and multiple selections.
You can find it here: http://code.google.com/p/gong-wpf-dragdrop/
I’d love to get some feedback on this!
November 7, 2009 at 7:01 am
Bea
Hi Groky,
Your solution adds some very useful features to the one in this post. It’s nice that it works with MVVM and TreeViews.
Thanks so much for sharing it with the community!
Bea
November 17, 2009 at 3:33 pm
Groky
Thank you Bea.
I was wondering if you had time to give your thoughts on a problem I am facing with the drag drop framework and multiple selections. I have asked in various places but received no useful replies.
The problem is that when multiple items are selected in an ItemsControl, a click on an already selected item deselects the rest of the items selected on that control. So to allow the user to drag a multiple selection, in the preview mouse down event I check to see if the click is on a control with multiple selection enabled, and if the click is on an already selected item. If both of these conditions are met, I set e.Handled = true in the event handler so that the event is not received by the ItemsControl.
This allows the user to drag multiple items, but it also has a couple of side effects:
- If the user has selected all of the items in the control, there is no way to deselect the items.
- The control no longer responds to double clicks.
Do you have any advice on how to handle this any better? You can see this in action in the DefaultsExample project in the framework.
Thanks,
Steven
December 2, 2009 at 6:18 am
Bea
Hi Steven,
You can only differentiate the drag scenario from the deselect scenario by using a combination the 3 events: MouseDown, MouseMove, MouseUp. You can select on MouseDown, initiate a drag when you get a MouseDown + MouseMove, and deselect on MouseUp.
Let me know if this helps.
Bea
December 3, 2009 at 10:41 am
Groky
So I would essentially have to take on all responsibility for clicks on a drag-enabled control? For example, what about the double click case?
December 10, 2009 at 2:18 am
Bea
Right.
Same thing for the double click. You would have to detect that case (either with Control.MouseDoubleClick if you’re dealing with a control or by looking at the ClickCount property if you’re not), and deal with that scenario specifically.
Bea
January 20, 2010 at 6:19 pm
RandomEngy
You have a small bug: normally ListView items have 1px of space between them. In your case the image of the planet is somehow making it go away, so the insert location is always correct. However, with the 1 px of space (as is normally the case), if the mouse is directly between 2 items, it will insert at the end of the list. The same happens when you’re hovering over the GridView headers: it tries to put the item at the end of the list.
January 3, 2010 at 5:49 pm
Bea
Hi,
The code assumes that if the mouse is over a ListViewItem, it will figure out a way to insert the item before or after the hovered item. If it’s not, then it inserts the item at the end of the list. The problem you see (which I can repro) is that the default style for ListViewItem introduces a 1 px margin around it, so when you’re hovering that 1 px area, you’re not really over a ListViewItem.
One solution to this problem is to remove the margin from the ListViewItem. This can be easily done by adding a style to ListViewItem that sets its margin to 0.
<Style TargetType=”{x:Type ListViewItem}” x:Key=”ListViewItemStyle”>
<Setter Property=”Margin” Value=”0″/>
</Style>
<ListView ItemContainerStyle=”{StaticResource ListViewItemStyle}” …
If you want that 1px space to remain for aesthetical reasons, instead of a margin you can add it as padding, or a border.
Hope this helps.
Bea
January 21, 2010 at 3:53 pm
Zach
I tried extending the visual part of the data template a bit (see below). This mostly works like I expected. Except where it doesn’t. Specifically, the label that I added, if I click on that portion of the visible item it does not find a sourceItemContainer and does not initiate the drag/drop operation. If I click in the space around the label (but not on the label), then it does work. In the case where it does not work there the itemContainerVerify is null so it doesn’t match the itemContainer.
I suspect this has something to do with the code comment “this method makes no assumption about the type of that container. (It will get a ListBoxItem if it is a ListBox …”. But it is unclear how I work around this. Any help is appreciated.
January 17, 2010 at 1:58 pm
Zach
code didn’t get included. basically I have
Border
StackPanel
Label
Image
/StackPanel
/Border
January 17, 2010 at 1:59 pm
Bea
Hi Zach,
Yep, I can repro what you describe (you can see my code in this project), and understand what is going on.
The code in “GetItemContainer” first gets the type of container for the ItemsControl, which in this case is ContentPresenter. Then it walks up the visual tree, starting from the bottom-most element clicked (in this case, the Label), until it finds the first element of type ContentPresenter. Then it checks that the ContentPresenter found is in fact a container of the ItemsControl passed as a parameter. This is where things go wrong. The problem is that Label’s default template contains a ContentPresenter, and this one is encountered first when walking up the tree. So the code failes when verifying that it is a container of the ItemsControl.
There are a few ways in which the code can be fixed:
- You can use a TextBlock instead of a Label.
- You can modify the code in “GetItemContainer”. Here’s the code that checks whether the container belongs to the ItemsControl (the comparison that fails in your scenario):
FrameworkElement itemContainerVerify = itemsControl.ItemContainerGenerator.ContainerFromItem(itemContainer.DataContext) as FrameworkElement;
if (itemContainer != itemContainerVerify)
{
itemContainer = null;
}
If the verification that the container belongs to the ItemsControl fails, you can continue walking up the tree looking for the next container of the same type, and repeat the comparison. This would be done until the container in fact belongs to the ItemsControl or the tree walk reaches the top window.
Let me know if this makes sense.
Thanks,
Bea
January 21, 2010 at 10:42 am
Zach
Rock and Roll. Thanks. I wrote a combined version of GetItemContainer and FindAncestor so it essentially does the FindAncestor loop inside GetItemContainer and then does the test for the container belonging to the itemsControl inside the loop. Works great. Less filling. Thanks.
January 26, 2010 at 8:48 am
Bea
Hi Zach,
FYI: I have updated the source code of this post with a fix for the issue you encountered.
Bea
March 5, 2010 at 11:05 am
Daniel
Hi Bea,
i get into troubles when i have to drag the same object multiple times into the same drop target. The underlying listbox can’t make differences between the items and selected them all.
To fix this i used die IClonable Interface in your InsertItemInItemsControl. Hope this can help someone else.
if (itemToInsert != null)
{
IEnumerable itemsSource = itemsControl.ItemsSource;
if (itemsSource == null)
{
itemsControl.Items.Insert(insertionIndex, itemToInsert);
}
// Is the ItemsSource IList or IList? If so, insert the dragged item in the list.
else if (itemsSource is IList)
{
/* New */
if (itemToInsert is ICloneable)
{
object copy = ((ICloneable)itemToInsert).Clone();
((IList)itemsSource).Insert(insertionIndex, copy);
}
else
{
((IList)itemsSource).Insert(insertionIndex, itemToInsert);
}/* New end */
}
else
{
Type type = itemsSource.GetType();
Type genericIListType = type.GetInterface(“IList`1″);
if (genericIListType != null)
{
type.GetMethod(“Insert”).Invoke(itemsSource, new object[] { insertionIndex, itemToInsert });
}
}
}
with kind regards
daniel
January 21, 2010 at 4:06 am
Bea
Hi Daniel,
Yeah, the fact that selection gets messed up when adding the same item twice to a ListBox is a well known and much discussed issue. When I was still on the team, we often talked about how common of a scenario that is, the priority of fixing it, and how we should fix it. Somehow the team never actually shipped a solution to that issue.
Your solution is good. Another alternative is to wrap the data items in containers. This way, even though the actual items may not be unique, the containers are.
Thanks for posting your solution! I’m sure it will be useful to others.
Bea
January 21, 2010 at 10:46 am
derek
Bea,
I have used this generic approach for our application and now the list items are becoming richer and richer…. to the extent that we have an AdornerDecorator which can be made visibleon occasions as part of the list item. The AdornerDecorator can contain other controls… and what I have noticed is that controls that rely on drag and drop themselves [e.g dragging a scroll bar, or drag -resizing a grid column] do not get an opportunity to handle their drag events before the DragDropHelper takes over…
Hope this makes sense.
Is there some way of making such an Adornerdecorator not allow the PreviewMouseDown, PreviewMouseMove etc to block these events temporarily?
Thanks for any insight you can give.
Derek
February 24, 2010 at 3:25 pm
Bea
Hi Derek,
I’m not sure that I fully understand your scenario. The drag and drop code is attaching handlers for the preview mouse events with the assumption that during a drag drop operation, we don’t want mouse events to affect anything else in the app. If you need to detach the preview mouse events temporarily in your app, you can use the standard event syntax:
element.PreviewMouseLeftButtonDown -= MyElement_PreviewMouseLeftButtonDown;
And to re-attach it, you can do the same thing with +=.
Bea
March 9, 2010 at 10:53 pm
Jalal
Thank you!
July 21, 2010 at 9:58 pm