Wednesday, January 5, 2011

[2011.01.05] Drag and Drop in WPF using MVVMLight [II/IV]

This is a multipart series outlining several topics is WPF
  1. Part 1: MVVM via the MVVMLight framework
  2. Part 2: Drag and Drop
  3. Part 3: The Adorner
  4. Part 4: DataTemplateSelector, StyleSelector, Unit Testing

Drag and Drop

MSDN has a section called Drag and Drop Overview but as far as I can tell, it only outlines what to do with the data you are moving around. You can also get more D&D info from:
Christian Mosers's post on WPF has the steps you need to follow to do Drag and Drop. I did mine slightly differently and I will list (re-hash) the steps he outlined basically, but for my scenario.
  1. Determine if you are initiating a drag/drop operation either as a combination of MouseLeftButtonDown and MouseMove or just MouseLeftButtonDown as I have done.
  2. Determine what data is to be dragged and store that in a DataObject by passing in a format and the data to the constructor. See MSDN's Drag and Drop Overview for more information regarding DataObject.
  3. Add adorner: If you want to provide rich visual feedback during the drag operation, adding an adorner can be done at this stage.
  4. Call the static DragDrop.DoDragDrop() passing in the source of the drag (a ListBoxItem in this case), the actual data to be dragged and an appropriate DragDropEffects enumeration [see MSDN].
    Note: Steps 1-4 can all be done in the MouseLeftButtonDown handler as I have outlined in the demo.
  5. Set the AllowDrop property on elements that you want to be able to drop on. In this case, I made both the source and target ListBoxs droppable. The property is set in the MainWindow.xaml file.
  6. Handle either the DragEnter or DragOver event. Test in this handler if you are passing over an element that has it's AllowDrop property set and that is accepts the type of data that is being dragged. Do this check by calling the GetDataPresent() method on the DragEventArgs parameter.
  7. Update Adorner: If you are using a custom adorner, this is a good place to update its position on the screen as it is dragged around.
  8. Finally, handle the Drop event when the user releases the mouse button. If the drop is done on a target that is expecting it and the correct data format is used, then retrieve the dragged DataObject and do whatever you need to with it at this point. You get the data by accessing the Data property on the DragEventArgs parameter and calling the GetData() method.
  9. Remove Adorner: If you are using a custom adorner, once the drop is complete, this is a good place to remove it.
Fig. 1 shows what the application looks like.

Fig.1 Drag and Drop

Let's take a closer look at each of the main points in the drag and drop operation. Listing 1 shows the markup with the ListBox elements and the associated events that necessary for D&D. Notice the use of the EventToCommand behavior here.

36 <ListBox x:Name="dragSource"
37 Grid.Row="1"
38 MinWidth="100"
39 MinHeight="40"
40 Margin="0,0,0,0"
41 AllowDrop="True"
42 Background="AliceBlue"
43 ItemContainerStyleSelector="{DynamicResource styleSelector}"
44 ItemsPanel="{DynamicResource SourceListBoxItemsPanelTemplate}"
45 ItemsSource="{Binding Contacts}"
46 ItemTemplateSelector="{DynamicResource dataTemplateSelector}"
47 Style="{DynamicResource ListBoxStyle1}">
48 <i:Interaction.Triggers>
49 <i:EventTrigger EventName="PreviewMouseLeftButtonDown">
50 <cmd:EventToCommand Command="{Binding PreviewMouseLeftButtonDownCommand, Mode=OneWay}" PassEventArgsToCommand="True" />
51 </i:EventTrigger>
52 <i:EventTrigger EventName="DragOver">
53 <cmd:EventToCommand Command="{Binding DragOverCommand, Mode=OneWay}" PassEventArgsToCommand="True" />
54 </i:EventTrigger>
55 <i:EventTrigger EventName="Drop">
56 <cmd:EventToCommand Command="{Binding DropSourceCommand, Mode=OneWay}" PassEventArgsToCommand="True" />
57 </i:EventTrigger>
58 </i:Interaction.Triggers>
59 </ListBox>
61 <Border Grid.Row="2" Height="20" />
63 <TextBlock Grid.Row="3"
64 Background="White"
65 FontSize="18"
66 Text="Listbox to drop to" />
68 <ListBox x:Name="dropTarget"
69 Grid.Row="4"
70 MinWidth="100"
71 MinHeight="117"
72 Margin="0,0,0,0"
73 AllowDrop="True"
74 Background="#FFE4F3FD"
75 ItemContainerStyleSelector="{DynamicResource styleSelector}"
76 ItemsPanel="{DynamicResource SourceListBoxItemsPanelTemplate}"
77 ItemsSource="{Binding TargetContacts}"
78 ItemTemplateSelector="{DynamicResource dataTemplateSelector}"
79 Style="{DynamicResource ListBoxStyle1}">
80 <i:Interaction.Triggers>
81 <i:EventTrigger EventName="Drop">
82 <cmd:EventToCommand Command="{Binding DropTargetCommand, Mode=OneWay}" PassEventArgsToCommand="True" />
83 </i:EventTrigger>
84 <i:EventTrigger EventName="DragOver">
85 <cmd:EventToCommand Command="{Binding DragOverCommand, Mode=OneWay}" PassEventArgsToCommand="True" />
86 </i:EventTrigger>
87 </i:Interaction.Triggers>
88 </ListBox>
Listing 1: Markup showing the ListBoxs and the events used for Drag and Drop

Steps 1-7: Drag

The PreviewMouseLeftButtonDown is a good place to determine if you want to begin a drag operation. The logic of what happens in this event is shown in Listing 2. In this case, I want to drag the data in a ListBoxItem, so first I needed to get a reference to the ListBoxItem that fired the mouse event as shown in Lines 76-77.
72 PreviewMouseLeftButtonDownCommand = new RelayCommand<MouseButtonEventArgs>(
73 e =>
74 {
75 // get dragged listbox
76 ListBox listBox = e.Source as ListBox;
77 ListBoxItem listBoxItem = VisualHelper.FindAncestor<ListBoxItem>((DependencyObject)e.OriginalSource);
79 // set up shared states
80 _listBoxItem = listBoxItem; // adorned element
81 _topLevelGrid = GetTopLevelGrid(e); // element to get an adorner layer from higher up the logical tree than the listboxitem
83 // Find the data behind the listBoxItem
84 if (listBox == null || listBoxItem == null) return;
86 Contact contact = (Contact)listBox.ItemContainerGenerator.ItemFromContainer(listBoxItem);
88 AddAdorner(listBoxItem, _topLevelGrid);
90 // Initialize the drag & drop operation
91 DataObject dragData = new DataObject("myContactData", contact);
92 DragDrop.DoDragDrop(listBoxItem, dragData, DragDropEffects.Move);
93 }
94 );
Listing 2: Command to deal with the dragging part of the operation.
In Line 77, I am using a helper method to actually pinpoint the correct ListBoxItem as shown in Listing 3. To create a DataObject, I first needed to grab the data contained in the ListBoxItem as shown in Line 86 by calling the ItemFromContainer method on the ItemContainerGenerator property of the ListBox. In Line 88, the adorner I used to help visualize the dragging operation is added. Lines 91-92 creates the DataObject to be dragged and then call the DoDragDrop method.
1 using System.Windows;
2 using System.Windows.Media;
4 namespace DragDropUsingMvvmLight01
5 {
6 /// <summary>
7 /// Walks up the visual tree to find the ancestor of a given type.
8 /// </summary>
9 internal static class VisualHelper
10 {
11 /// <summary>
12 /// Recursive method to walk up the visual tree to return an ancestor type of the supplied type.
13 /// </summary>
14 /// <typeparam name="T">Type of ancestor to search for.</typeparam>
15 /// <param name="current">Type to start search from.</param>
16 /// <returns></returns>
17 internal static T FindAncestor<T>(DependencyObject current) where T : DependencyObject
18 {
19 do
20 {
21 if (current is T)
22 {
23 return (T)current;
24 }
25 current = VisualTreeHelper.GetParent(current);
26 } while (current != null);
27 return null;
28 }
29 }
30 }
Listing 3: Helper method to find an element up the visual tree hierarchy.
Notice that both ListBoxes refer to the same DragOver event. This is because in both cases, all I am doing is checking to see if the dragged item is over an element it can drop on and at the same time, update the position of the adorner as it is moved across the screen. See Listing 4 for all this fun stuff
96 DragOverCommand = new RelayCommand<DragEventArgs>(
97 e =>
98 {
99 if (!e.Data.GetDataPresent("contact"))
100 {
101 e.Effects = DragDropEffects.None;
102 }
104 var currentMousePosition = e.GetPosition(_topLevelGrid);
106 if (_topLevelGrid != null && _draggedAdorner != null)
107 _draggedAdorner.UpdateAdornerPosition(_topLevelGrid, currentMousePosition);
108 }
109 );
Listing 4: Checking for a viable drop target and updating the adorner position.

Steps 8-9: Drop

Each ListBox raises its own version of the Drop event. Lines 111-122 deals with the expected case of dragging and dropping from source to target. First, check the DragEventArgs parameter to see if there is data there in the format we are expecting. This is shown in Line 114. If there is correct data, then simply update the collections of Contact items that is data bound to the respective ListBox. If there is a successful drop, then at this point, remove the adorner.

In Line 125 which deals with the case of doing a drag/drop on the source ListBox, there isn't much to do other than remove the adorner. See all this in Listing 5.
111 DropTargetCommand = new RelayCommand<DragEventArgs>(
112 e =>
113 {
114 if(!e.Data.GetDataPresent("myContactData")) return;
115 Contact contact = e.Data.GetData("myContactData") as Contact;
117 _targetContacts.Add(contact); // add to new collection
118 _contacts.Remove(contact); // remove from source collection
120 // pass in the root grid since its adorner layer was used to add ListBoxItems adorners to
121 RemoveAdorner(_listBoxItem, _topLevelGrid);
122 });
124 // if dropping on the source list, remove the adorner
125 DropSourceCommand = new RelayCommand<DragEventArgs>(e => RemoveAdorner(_listBoxItem, _topLevelGrid));
Listing 5: The drop operation.


Massimo said...

Thanks for this interesting post.
The source code link isn't available ... is it correct?


Anonymous said...

Great article! I want to bump the above question and ask for you to repost the source please.