Showing drag/drop feedback on the WPF adorner layer

Why adorners?
In the last sample, I showed how to allow the users to drag and drop shapes in a Canvas, and how to create a preview effect for the operation. One problem with this code is that it only works on Canvas panels.

In this post, we'll look into using the adorner layer in the window to draw our preview, so it works with any panel.

Cosmetic changes
OK, so what has changed? First, let's look at some cosmetic stuff.

I changed my top TextBlock to read as follows.

<TextBlock FontSize="8pt" FontFamily="Tahoma" TextWrapping="Wrap">
<Bold>Drag Canvas Sample
</Bold><LineBreak /><Run>
This sample demonstrates using adorners to drag shapes (or any other element).</Run>
</TextBlock>

I also added a TextBox in the Canvas, just to show off a bit.

<TextBox Canvas.Top="120" Canvas.Left="8" />

Creating a new Adorner
Now, let's create a new class of Adorner, that we'll use for our preview effect. To do this, I simply declared an inner class within Window1.

Class DropPreviewAdorner
Inherits Adorner

  ' Adding some basic fields to help us keep track of where we are and what we render
Private m_Child As Rectangle
Private m_LeftOffset As Double
Private m_TopOffset As Double

  ' Methods will come here.
' ...
End Class

The first to do when subclassing the Adorner class is to add the appropriate constructor.

Public Sub New(ByVal adornedElement As UIElement)
MyBase.New(adornedElement)

 Dim brush As VisualBrush = New VisualBrush(adornedElement)

 m_Child = New Rectangle()
m_Child.Width = adornedElement.RenderSize.Width
m_Child.Height = adornedElement.RenderSize.Height
m_Child.Fill = brush
End Sub

Now, many adorners will simply render their own content. Because we're lazy and we're using a Rectangle instead, we need to make sure the layout system knows about this and lays it out appropriately (we'll simply size to content).

Protected Overrides Function MeasureOverride(ByVal constraint As System.Windows.Size) As System.Windows.Size
m_Child.Measure(constraint)
Return m_Child.DesiredSize
End Function

Protected Overrides Function ArrangeOverride(ByVal finalSize As System.Windows.Size) As System.Windows.Size
m_Child.Arrange(New Rect(finalSize))
Return finalSize
End Function

Protected Overrides Function GetVisualChild(ByVal index As Integer) As System.Windows.Media.Visual
Return m_Child
End Function

Protected Overrides ReadOnly Property VisualChildrenCount() As Integer
Get
Return 1
End Get
End Property

With this bit of code, we can already show the rectangle we wanted.

We'll want to allow the drag/drop code we wrote in the window to update the adorner to follow the mouse, so we'll add a couple of properties for this.

Public Property LeftOffset() As Double
Get
Return m_LeftOffset
End Get
Set(ByVal value As Double)
m_LeftOffset = value
UpdatePosition()
End Set
End Property

Public Property TopOffset() As Double
Get
Return m_TopOffset
End Get
Set(ByVal value As Double)
m_TopOffset = value
UpdatePosition()
End Set
End Property

Private Sub UpdatePosition()
Dim adornerLayer As AdornerLayer = Me.Parent
If Not adornerLayer Is Nothing Then
adornerLayer.Update(AdornedElement)
End If
End Sub

Finally, adorners are always placed relative to the element they adorn. You can then place them relative to the corners, the middle, off to the side, within, etc. We'll just offset the adorner to where the user would like to drop the dragged element, by adding a translate transform to whatever was necessary to get to the adorned element.

Public Overrides Function GetDesiredTransform(ByVal transform As System.Windows.Media.GeneralTransform) As System.Windows.Media.GeneralTransform
Dim result As GeneralTransformGroup = New GeneralTransformGroup()
result.Children.Add(MyBase.GetDesiredTransform(transform))
result.Children.Add(New TranslateTransform(LeftOffset, TopOffset))
Return result
End Function

Using our new Adorner
To leverage our new adorner, we simply need to update the drag/drop code we wrote in the last sample. First, let's change the type of the overlay element.

Private m_OverlayElement As DropPreviewAdorner

Now, when we start dragging, instead of creating a rectangle, we'll create our adorner.

Private Sub DragStarted()
m_IsDragging = True

 m_OriginalLeft = Canvas.GetLeft(m_OriginalElement)
m_OriginalTop = Canvas.GetTop(m_OriginalElement)

 Dim layer As AdornerLayer
m_OverlayElement = New DropPreviewAdorner(m_OriginalElement)
layer = AdornerLayer.GetAdornerLayer(m_OriginalElement)
layer.Add(m_OverlayElement)
End Sub

When we finish, we'll use the offsets we store in the adorner to update the position of the moved element, and we'll also remove the adorner from the tree.

Private Sub DragFinished(ByVal canceled As Boolean)
System.Windows.Input.Mouse.Capture(Nothing)
If m_IsDragging Then
AdornerLayer.GetAdornerLayer(m_OverlayElement.AdornedElement).Remove(m_OverlayElement)
If Not canceled Then
Canvas.SetLeft(m_OriginalElement, m_OriginalLeft + m_OverlayElement.LeftOffset)
Canvas.SetTop(m_OriginalElement, m_OriginalTop + m_OverlayElement.TopOffset)
End If
m_OverlayElement = Nothing
End If
m_IsDragging = False
m_IsDown = False
End Sub

And, finally, while we are dragging, we'll update our new adorner instead of the old rectangle.

Private Sub DragMoved()
Dim currentPosition As Point = System.Windows.Input.Mouse.GetPosition(MyCanvas)
m_OverlayElement.LeftOffset = currentPosition.X - m_StartPoint.X
m_OverlayElement.TopOffset = currentPosition.Y - m_StartPoint.Y
End Sub

Was that easy or what? Now you can run the example, and you can again drag and drop everything. For fun, try changing the Canvas to a DockPanel. Because we haven't written any code to accomodate this layout, the dragging and dropping won't behave as expected, but you'll still be able to see the adorner correctly following the mouse.

Final touch
As a final touch, we can animate the opacity of the visual brush we're using - this is very simple to do. Just add the following lines in the constructor for our DropPreviewAdorner to add a local animation.

Public Sub New(ByVal adornedElement As UIElement)
...
m_Child.Fill = brush

 Dim animation As System.Windows.Media.Animation.DoubleAnimation
animation = New System.Windows.Media.Animation.DoubleAnimation(0.3, 1, New Duration(TimeSpan.FromSeconds(1)))
animation.AutoReverse = True
animation.RepeatBehavior = System.Windows.Media.Animation.RepeatBehavior.Forever
brush.BeginAnimation(System.Windows.Media.Brush.OpacityProperty, animation)
End Sub

Now you should get a cool fade-in / fade-out effect on the preview for the drop operation.

Update on 2006-10-19: to read about a gotcha in GetDesiredTransform, read https://blogs.msdn.com/marcelolr/archive/2006/10/19/wpf-dragdrop-sample-and-viewbox-beware.aspx 

Update on 2010-04-28: updated links to MSDN - latest version used when needed, but you can see this has been around for a while.

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.