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 http://www.microsoft.com/info/cpyright.htm.

Comments (2)

  1. freeflyr says:

    Hey Marcelo,

    Do you have a working project for this that you could possibly post?  Either i’m missing a reference or this is based on a different version of the framework to the one i’m running (feb ctp) because i can’t find the object DropPreviewAdorner.

    Thanks in advance

  2. Some time back Marcelo had written a series of posts covering drag and drop. This was interesting reading

Skip to main content