DataObject-based drag+drop+preview

Using a DataObject
I've blogged about data objects in the past (here and here for example). You can use them for clipboard and drag/drop operations between applications. In this installment, we're going to switch over to using a DataObject, and will thus be able to move shapes into other instances of our app, plus pick up a few cool tricks along the way.

Restore that Canvas!
If you've been playing with the sample we had last week, be sure to restore that Canvas as the container for our shapes - the effect is more impressive this way. Also, we need to add an AllowDrop property, so the tag opening line should look like this.

<Canvas Name="MyCanvas" AllowDrop="True">

Cleaning up our code
First off, let's remove some of the bookkeeping code that we will no longer need. Some of this will be replaced by new functionality, other is simply maintained for us by the system.

Let's start by removing the m_OriginalLeft, m_OriginalTop and m_IsDragging fields. In place of them, we'll add an m_RemoteElement (used to keep track of elements from another application) and m_MyFormat (used to register our very own clipboard format. The fields section should look like this.

Private m_StartPoint As Point
Private m_IsDown As Boolean
Private m_OriginalElement As UIElement
Private m_OverlayElement As DropPreviewAdorner
Private m_RemoteElement As UIElement
Private Shared m_MyFormat As DataFormat = _
DataFormats.GetDataFormat("My Love-ly Format")

Now, let's remove some methods. Window1_PreviewKeyDown can go - we get this for free. DragMoved can go as well - we'll add this functionality in another handler.

Updating our DropPreviewAdorner
Our adorner was good enough for out last post, but now we're going to face a slightly different situation. Because the element we are going to add may come from a different application, we can't put an adorner that's relative to it. Instead, we'll make a distinction between what we adorn (say, the whole contents of the window) and what we adorn *with* (a preview of the element the user is moving). Let's update the constructor for our adorner to look like this.

Public Sub New(ByVal adornedElement As UIElement, _
ByVal adorningElement As UIElement)
MyBase.New(adornedElement)

  ' By default, an adorner will be sized the same
' as its target element, and positioned the
' same as its target element. Works beautifully for us. :)
Dim brush As VisualBrush = New VisualBrush(adorningElement)

  m_Child = New Rectangle()
m_Child.Width = adorningElement.RenderSize.Width
m_Child.Height = adorningElement.RenderSize.Height
m_Child.Fill = brush
m_Child.IsHitTestVisible = False

  ' For extra effect, we'll animate the opacity of the brush.
...
End Sub

We've added a new argument, which we use to create a visual brush from, and we're also setting the IsHitTestVisible property on the rectangle we're moving about, as it now lives in a different place in the visual tree of our application and can interfere with the mouse if we don't do this.

Supporting drag - writing the provider
With less bookkeeping, our mouse move handler is simplified.

Private Sub MyCanvas_PreviewMouseMove(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles MyCanvas.PreviewMouseMove
If m_IsDown Then
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
DragStarted()
End If
End If
End Sub

DragStarted(), however, will be doing a bit more work. Its purpose is to populate a DataObject, invoke DragDrop.DoDragDrop, and do something with its results (in effect, the whole implementation is here).

We can use the XamlWriter to create an easy-to-move-around representation of our object, and we can use the drag/drop flags to determine what can happen and what has effectively happened.

Private Sub DragStarted()
m_IsDown = False

  Dim serializedObject As String = System.Windows.Markup.XamlWriter.Save(m_OriginalElement)
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)

  ' Note: the following line won't execute until the whole drag/drop operation has taken place.
If effects And DragDropEffects.Move Then
' Remove the element.
MyCanvas.Children.Remove(m_OriginalElement)
m_OriginalElement = Nothing
End If
End Sub

Supporting drop - writing the consumer
Writing the consumer is more involved than writing the provider. We need to determine whether we even understand the source of the data, update the UI with our preview effect, and do the work on our side to carry out the operation.

In order of runtime execute, these are the events we will handle: PreviewDragEnter, PreviewDragOver, Drop. PreviewDragLeave will be called if the user just leaves our drop area and moves on.

Here's the implementation for PreviewDragEnter.

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 from the stream.
Dim serializedObject As String = e.Data.GetData(m_MyFormat.Name)
Dim reader As System.Xml.XmlReader = System.Xml.XmlReader.Create( _
New System.IO.StringReader(serializedObject), _
New System.Xml.XmlReaderSettings())
m_RemoteElement = System.Windows.Markup.XamlReader.Load(reader)

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

  e.Handled = True
End Sub

In the body of this method, we're relying on a helper method we'll leave for later, UpdateEffects. This method tells us whether we should even bother with this data source, and updates what we think should happen if the user drops the data in our application. We use XamlReader as the counterpart of XamlWriter, and use the element from the other application (m_RemoteElement) to paint our preview.

PreviewDragOver is very simple, and moves the adorner about. Note that because we're adorning Me.Content, we make sure to get an offset relative to Me.Content.

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

  Dim currentPosition As Point = e.GetPosition(Me.Content)
m_OverlayElement.LeftOffset = currentPosition.X
m_OverlayElement.TopOffset = currentPosition.Y

  e.Handled = True
End Sub

PreviewDrop is more involved. In this method, we'll add the element to our application and clean up after ourselves. Note that because we were tracking our offset relative to Me.Content, we need to use a transform to figure out what the offset is relative to MyCanvas, which is where we'll add the element.

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

  ' Add the element.
MyCanvas.Children.Add(m_RemoteElement)
Dim t As System.Windows.Media.GeneralTransform = TransformToDescendant(MyCanvas)
Dim dropPoint As Point = e.GetPosition(Me.Content)
dropPoint = t.Transform(dropPoint)
Canvas.SetLeft(m_RemoteElement, dropPoint.X)
Canvas.SetTop(m_RemoteElement, dropPoint.Y)

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

  e.Handled = True
End Sub

PreviewDragLeave is like the cleanup section of PreviewDrop. I could have placed this code in a helper method, but I didn't want to break this sample up any further for clarity.

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

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

  e.Handled = True
End Sub

Finally, our helper method, UpdateEffects. This has two parts: figuring out whether we can do anything with the given data object (we need to understand the data contents and the operation), and figuring out what we will end up doing if the user drops (copying or moving).

Private Function UpdateEffects(ByVal e As System.Windows.DragEventArgs) As Boolean
' If we don't know what we're talking about, we shouldn't do anything.
If Not e.Data.GetDataPresent(m_MyFormat.Name) Then
e.Effects = DragDropEffects.None
Return False
End If

  ' If we can't copy or move, we shouldn't do anything (eg: provider wants us to link).
If (e.AllowedEffects And DragDropEffects.Copy) = 0 AndAlso _
(e.AllowedEffects And DragDropEffects.Move) = 0 Then
e.Effects = DragDropEffects.None
Return False
End If

  ' Figure out whether we should copy or move. If we can do either, we'll move unless
' Ctrl is pressed.
If (e.AllowedEffects And DragDropEffects.Copy) <> 0 AndAlso _
(e.AllowedEffects And DragDropEffects.Move) <> 0 Then
If (e.KeyStates And DragDropKeyStates.ControlKey) <> 0 Then
e.Effects = DragDropEffects.Copy
Else
e.Effects = DragDropEffects.Move
End If
Else
e.Effects = e.AllowedEffects And Not (DragDropEffects.Copy Or DragDropEffects.Move)
End If
Return True
End Function

And with this, we've finished the drop section of the protocol. Now we're ready to rock n' roll!

Drag between app instances
To get the coolest effect, you probably want to build and fire up two instances of our sample application. Then drag items from one window to another, one window to itself, and try pressing and releasing the Ctrl key while you do the operations to see the different effects.

 

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.