WPF drag/drop - it's all about the data

Dragging UIElements around is all good n' nice, but applications are often interested in the data itself more than in any particular representation we might have for it.

Declaring our data
For this sample, we will simply embed XML in an XmlDataProvider resource. We'll also add a template that describes how to turn our data into something that can be laid out and rendered.

To do this, add this bit of XAML right under the Window tag in Window1.xaml.

<Window.Resources>
<XmlDataProvider x:Key="MyData">
<x:XData>
<Sections xmlns="" Title="Library Favorites">
<Section Name=".NET Development">
<Article Name="Articles and Overview" />
<Article Name=".NET Performance" />
<Article Name="Windows Vista" />
<Article Name="XML and the .NET Framework" />
</Section>
</Sections>
</x:XData>
</XmlDataProvider>
<DataTemplate x:Key="ArticleTemplate">
<TextBlock FontSize="10pt" Text="{Binding
XPath=@Name }" />
</DataTemplate>
</Window.Resources>

Changes to the user interface
If you want to keep the sample description consistent, you can change the TextBlock to this.

<TextBlock FontSize="8pt" FontFamily="Tahoma" TextWrapping="Wrap">
<Bold>Drag Sample
</Bold><LineBreak /><Run>
This sample demonstrates using a DataObject for dragging "pure" data.</Run>
</TextBlock>

Finally, let's rip out our Canvas, and put a ListBox instead. Out of convenience for readers that are following along, I've still named this MyCanvas, to minimize the number of changes that need to be made.

<ListBox Name="MyCanvas" AllowDrop="True"
ItemsSource="{Binding Source={StaticResource MyData}, XPath=/Sections/Section/Article}"
ItemTemplate="{StaticResource ArticleTemplate}">
<ListBox.Background>
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>
<GradientStop Color="White" Offset="0" />
<GradientStop Color="DarkBlue" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</ListBox.Background>
</ListBox>

From UIElement to XmlElement
OK, we're set now to start changing what kind of data we actually handle. Farewell UIElements! Welcome, XmlElements! This approach simplifies the design for an application: no longer tied to a specific UIElement implementation, easier validation, less security concerns, etc.

To begin with, we'll change m_OriginalElement and m_RemoteElement to be XmlElement fields.

...
Private m_OriginalElement As System.Xml.XmlElement
Private m_OverlayElement As DropPreviewAdorner
Private m_RemoteElement As System.Xml.XmlElement
...

Changes to the provider side
Let's change the provider side of our drag/drop operations to use our app-specific XML instead of generic Xaml. The first place to start is on the mouse down handler.

This handler will try to find the XmlElement that corresponds to wherever the user is pressing the mouse-down button, and then capture some information about where it was pressed.

Private Sub MyCanvas_PreviewMouseLeftButtonDown(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) Handles MyCanvas.PreviewMouseLeftButtonDown
m_OriginalElement = GetElementFromPoint(MyCanvas, e.GetPosition(MyCanvas))
If m_OriginalElement Is Nothing Then Exit Sub
m_IsDown = True
m_StartPoint = e.GetPosition(MyCanvas)
End Sub

Note that it uses a helper method, GetElementFromPoint. This method will walk up the visual tree until it finds an element that matches a data item, and return Nothing if the user clicked somewhere where there is no item.

Private Function GetElementFromPoint(ByVal box As ListBox, ByVal point As Point) As System.Xml.XmlElement
Dim element As UIElement = box.InputHitTest(point)
While True
If element Is box Then Return Nothing
Dim item As Object = box.ItemContainerGenerator.ItemFromContainer(element)
Dim itemFound As Boolean = Not item.Equals(DependencyProperty.UnsetValue)
If itemFound Then Return item
element = VisualTreeHelper.GetParent(element)
End While
Return Nothing
End Function

Finally, in the DragStarted method, we'll change the way we generate the data we place on the data object, and remove an XmlElement from an XML tree rather than a UIElement.

Private Sub DragStarted()
m_IsDown = False

  Dim serializedObject As String = m_OriginalElement.OuterXml
Dim data As DataObject = New DataObject()
data.SetData(m_MyFormat.Name, serializedObject)
Dim effects As DragDropEffects = _
DragDrop.DoDragDrop(MyCanvas, data, DragDropEffects.Copy Or DragDropEffects.Move)
If effects And DragDropEffects.Move Then
' Remove the element.
m_OriginalElement.ParentNode.RemoveChild(m_OriginalElement)
m_OriginalElement = Nothing
End If
End Sub

Done! Now, on to the other side of the fence...

Changes to the consumer side
The changes here are a bit more involved, but not by much. First, when the mouse enters during a drag operation, we need to load an XML document rather than a WPF UIElement. Next, we need to transform that piece of data into something we can use to preview the drop operation, and here we can reuse our beloved data template.

Private Sub MyCanvas_PreviewDragEnter(ByVal sender As Object, ByVal e As System.Windows.DragEventArgs) Handles MyCanvas.PreviewDragEnter
If Not UpdateEffects(e) Then
Exit Sub
End If

  ' First, we deserialize the object provided to us.
Dim serializedObject As String = e.Data.GetData(m_MyFormat.Name)
Dim document As System.Xml.XmlDocument = New System.Xml.XmlDocument()
document.LoadXml(serializedObject)
m_RemoteElement = document.DocumentElement

  ' Now, create something we can render with.
Dim presenter As ContentPresenter = New ContentPresenter()
presenter.Content = m_RemoteElement
presenter.ContentTemplate = MyCanvas.ItemTemplate

  ' Next, create an adorner for it.
Dim layer As AdornerLayer
m_OverlayElement = New DropPreviewAdorner(Me.Content, presenter)
layer = AdornerLayer.GetAdornerLayer(Me.Content)
layer.Add(m_OverlayElement)

  e.Handled = True
End Sub

In PreviewDragLeave, I've simplified this a bit.

Private Sub MyCanvas_PreviewDragLeave(ByVal sender As Object, ByVal e As System.Windows.DragEventArgs) Handles MyCanvas.PreviewDragLeave
If m_OverlayElement Is Nothing Then Exit Sub

  AdornerLayer.GetAdornerLayer(Me.Content).Remove(m_OverlayElement)
m_OverlayElement = Nothing
m_RemoteElement = Nothing

  e.Handled = True
End Sub

In the PreviewDrop event, the only bit of code that needs to be changed is the way in which we put the content into our application.

The handler will find the XML document we're using, then invoke ImportNode (XML nodes are specific to a given document, in this case the temporary one we loaded in the PreviewDragEnter handler), and append it to the section with the articles.

Private Sub MyCanvas_PreviewDrop(ByVal sender As Object, ByVal e As System.Windows.DragEventArgs) Handles MyCanvas.PreviewDrop
...
' Add the element.
Dim dataProvider As XmlDataProvider = FindResource("MyData")
Dim document As System.Xml.XmlDocument = dataProvider.Document
Dim node As System.Xml.XmlElement = dataProvider.Document.ImportNode(m_RemoteElement, True)
dataProvider.Document.GetElementsByTagName("Section")(0).AppendChild(node)
...
End Sub

Fixing an oopsie
When detecting whether we should initiate a drag operation in the PreviewMouseMove operation, we used this check.

If Math.Abs(e.GetPosition(MyCanvas).X - m_StartPoint.X) > SystemParameters.MinimumHorizontalDragDistance AndAlso _
Math.Abs(e.GetPosition(MyCanvas).Y - m_StartPoint.Y) > SystemParameters.MinimumVerticalDragDistance Then
...

What we really want, however, is to move if the user moves far in enough in either direction, not in both directions.

If Math.Abs(e.GetPosition(MyCanvas).X - m_StartPoint.X) > SystemParameters.MinimumHorizontalDragDistance OrElse _
Math.Abs(e.GetPosition(MyCanvas).Y - m_StartPoint.Y) > SystemParameters.MinimumVerticalDragDistance Then
...

Done!
Again, for fun, try running multiple instances of the application, dragging elements around, and pressing or releasing Ctrl to control whether a copy is made of the element or not.

 

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.