Basics of data-bound drag-drop in WPF

Also known as "how do I perform drag-drop between two data-bound list boxes"? Well, excellent question - here's a first shot at the answer, although there is much polish that could be added.

This really isn't all that different from the series that was started a long time ago. Let's start with a simple WPF project and add this to the main window.

<Window x:Class="BoundListBoxDragDrop.Window1"
xmlns="
https://schemas.microsoft.com/winfx/2006/xaml/presentation "
xmlns:x="
https://schemas.microsoft.com/winfx/2006/xaml "
Title="Window1" Height="300" Width="300" Loaded="Window_Loaded">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.ColumnSpan="2" TextWrapping="WrapWithOverflow">
The ListBox controls are bound to ObservableCollection&lt;string&gt; collections.
<LineBreak />
Drag and drop works great!
</TextBlock>
<ListBox Grid.Row="1" Name="ListBox1" />
<ListBox Grid.Row="1" Grid.Column="1" Name="ListBox2" />
</Grid>
</Window>

Now, we'll add some fake data in the loaded method, and prepare the ListBox instances to deal be drag sources and targets.

private void Window_Loaded(object sender, RoutedEventArgs e)
{
ListBox1.ItemsSource = new ObservableCollection<string>(new List<string>(new string[] { "Red", "Green", "Blue" }));
ListBox2.ItemsSource = new ObservableCollection<string>(new List<string>(new string[] { "North", "South", "East", "West" }));

    PrepareListBoxForDragDrop(ListBox1);
PrepareListBoxForDragDrop(ListBox2);
}

The PrepareListBoxForDragDrop will mostly wire up events we're interested in to handle the flow of things.

private void PrepareListBoxForDragDrop(ListBox listbox)
{
listbox.AllowDrop = true;
listbox.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(ListboxPreviewMouseLeftButtonDown);
listbox.PreviewMouseMove += new MouseEventHandler(ListboxPreviewMouseMove);
listbox.PreviewMouseLeftButtonUp += new MouseButtonEventHandler(ListboxPreviewMouseLeftButtonUp);
listbox.Drop += new DragEventHandler(ListboxDrop);
}

Now, we'll take this bit by bit, but because I'm feeling extra-lazy today, I decided to make sure that every ListBox had its own set of variables and not scope everything to the window like I did before, so here are a few DependencyProperty declarations to the rescue.

private static DependencyProperty DraggingElementProperty = DependencyProperty.RegisterAttached(
"DraggingElement", typeof(string), typeof(Window1), new FrameworkPropertyMetadata(null));
private static DependencyProperty IsDownProperty = DependencyProperty.RegisterAttached(
"IsDown", typeof(bool), typeof(Window1), new FrameworkPropertyMetadata(false));
private static DependencyProperty StartPointProperty = DependencyProperty.RegisterAttached(
"StartPoint", typeof(Point), typeof(Window1), new FrameworkPropertyMetadata(default(Point)));

The whole thing starts when the user presses the mouse button, so let's start our code from there.

private void ListboxPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
ListBox listbox = (ListBox)sender;
string s = GetBoundItemFromPoint(listbox, e.GetPosition(listbox));
if (s != null)
{
listbox.SetValue(IsDownProperty, true);
listbox.SetValue(DraggingElementProperty, s);
listbox.SetValue(StartPointProperty, e.GetPosition(listbox));
}
}

GetBoundItemFromPoint will be familiar to readers of this blog, although here's a slightly modified version for C# rather than VB.Net.

private string GetBoundItemFromPoint(ListBox box, Point point)
{
UIElement element = box.InputHitTest(point) as UIElement;
while (element != null)
{
if (element == box)
{
return null;
}
object item = box.ItemContainerGenerator.ItemFromContainer(element);
bool itemFound = !object.ReferenceEquals(item, DependencyProperty.UnsetValue);
if (itemFound)
{
return (string)item;
}
else
{
element = VisualTreeHelper.GetParent(element) as UIElement;
}
}
return null;
}

Now that we've stored all the information about the operation that we're about to start, it's time to see what happens when the user starts to move the mouse with the button pressed.

private void ListboxPreviewMouseMove(object sender, MouseEventArgs e)
{
ListBox listbox = (ListBox)sender;

    bool isDown = (bool)listbox.GetValue(IsDownProperty);
if (!isDown)
{
return;
}

    Point startPoint = (Point)listbox.GetValue(StartPointProperty);

    if (Math.Abs(e.GetPosition(listbox).X - startPoint.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(e.GetPosition(listbox).Y - startPoint.Y) > SystemParameters.MinimumVerticalDragDistance)
{
DragStarted(listbox);
}
}

DragStarted will also be familiar to readers - I'm following the same pattern I've done in the past, as user expectations don't vary much in this regard (the actual details of how we choose to determine what to drag and how to move it around do, however).

private void DragStarted(ListBox listbox)
{
listbox.ClearValue(IsDownProperty); // SetValue to false would also work.

    // Add the bound item, available as DraggingElement, to a DataObject, however we see fit.
string draggingElement = (string)listbox.GetValue(DraggingElementProperty);
DataObject d = new DataObject(DataFormats.UnicodeText, draggingElement);
DragDropEffects effects = DragDrop.DoDragDrop(listbox, d, DragDropEffects.Copy | DragDropEffects.Move);
if ((effects & DragDropEffects.Move) != 0)
{
// Move rather than copy, so we should remove from bound list.
ObservableCollection<string> collection = (ObservableCollection<string>)listbox.ItemsSource;
collection.Remove(draggingElement);
}
}

The Drop case is very simple at this point.

private void ListboxDrop(object sender, DragEventArgs e)
{
ListBox listbox = (ListBox)sender;
ObservableCollection<string> collection = (ObservableCollection<string>)listbox.ItemsSource;
if (e.Data.GetDataPresent(DataFormats.UnicodeText, true))
{
collection.Add((string)e.Data.GetData(DataFormats.UnicodeText, true));
e.Effects =
((e.KeyStates & DragDropKeyStates.ControlKey) != 0) ?
DragDropEffects.Copy : DragDropEffects.Move;
e.Handled = true;
}
}

And, of course, we should handle the case where the user clicks but never intends to drag anything.

private void ListboxPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
ListBox listbox = (ListBox)sender;
listbox.ClearValue(IsDownProperty);
listbox.ClearValue(DraggingElementProperty);
listbox.ClearValue(StartPointProperty);
}

I've purposefully made this interoperable with other progrems, so you can drag and drop strings to and from other programs, including Microsoft Word or Microsoft Visual Studio. If I wanted to keep this just between my programs, I would probably have opted for a custom data format.

In short, this isn't very different from other cases we've seen so far. The interesting bits are updating the collection that the ListBox is bound to, which is something that I've done in a very direct manner for this simple sample, but that wouldn't be too hard to adapt to more generic collections, by using the different binding interfaces that are available.

 

This posting is provided "AS IS" with no warranties, and confers no rights. Use of included script samples are subject to the terms specified at https://www.microsoft.com/info/cpyright.htm.