Thursday, January 5, 2023

A better WPF drag/drop experience

The WPF Drag/Drop experience leaves something to be desired. If you're dragging something, the cursor should indicate what is being dragged. Infragistics has a nice Drag/Drop extension using behaviors but it only initiates a drag operation when the user clicks the left mouse button. That means you can't drag editable controls such as text boxes and combo boxes.

I want a drag/drop operation with these features.

  • Initiate a drag with any mouse button
  • Display an image of the dragged control while dragging
  • Tell potential drop targets when the mouse enters and leaves them
  • Change the cursor to indicate when it is over a potential drop target
  • Minimal XAML and code-behind to implement drag/drop

I had to solve these problems

  • Create an image of the control as it is dragged. I used a VisualBrush.
  • Float the image over the page. I used an adorner
  • Move the image to track the mouse. I used a TranslateTransform.
  • Mark a control as a valid drag source or drop trarget. I used behaviors.
  • Call methods in the code-behind when needed. I used reflection.
Dragging the top control over the third one

This is a complex project, but it's very reusable. Start a new Visual Studio VB .Net Framework project and call it DragControl. Add a DragDrop and a DragDropAdorner class. These two classes are what you will put in a project that needs this functionality.

We will start with the adorner class. This allows us to put graphics over a control. By assigning a TranslateTransform to the adorner we can move it relative to the control it adorns. The control being dragged needs to capture the mouse during the drag operation. Our adorner will handle those capture and release events to make itself visible or collapsed. Note the used of the VisualBrush to create an image of the control being adorned. Here is the complete DragDropAdorner class. We will use it  to create a DragDropAdorner to add the the adornerLayer of the control being dragged.

Public Class DragDropAdorner
    Inherits Adorner
 
    Public Sub New(adornedElement As UIElement)
        MyBase.New(adornedElement)
 
        AddHandler adornedElement.GotMouseCapture, AddressOf adornedElement_GotMouse
        AddHandler adornedElement.LostMouseCapture, AddressOf adornedElement_LostMouse
        Me.IsHitTestVisible = False
    End Sub
 
    Private Sub adornedElement_LostMouse(sender As Object, e As MouseEventArgs)
        Me.Visibility = Visibility.Collapsed
    End Sub
 
    Private Sub adornedElement_GotMouse(sender As Object, e As MouseEventArgs)
        Me.Visibility = Visibility.Visible
    End Sub
 
    Protected Overrides Sub OnRender(drawingContext As DrawingContext)
 
        MyBase.OnRender(drawingContext)
        SetBitmapSource(drawingContext, Me.AdornedElement)
    End Sub
 
    Private Shared Sub SetBitmapSource(ctx As DrawingContext, target As Visual)
        If ctx Is Nothing OrElse target Is Nothing Then Exit Sub
 
        Dim bounds As Rect = VisualTreeHelper.GetDescendantBounds(target)
        Dim vb As New VisualBrush(target)
        ctx.DrawRectangle(vb, Nothing, New Rect(New Point(), bounds.Size))
    End Sub
 
End Class

Now we need to write the DragDrop class. It exposes two attached properties called DragSource and DropTarget. These take a DragSource and DropTarget object respectively. Although these objects are bindable, their individual properties are not. Let's start by defining some enumerables and those two classes.

DragSource.Initiator - an enumeration that specifies which mouse buttons can initiate a drag
DragSource.Drop - the name of the method in code-behind to call when this source is dropped
DragSource.VisualSource (read only) - the control this behavior is attached to
DragSource.ErrorHandler - an enumeration that specifies how errors are to be handled

DropTarget.Enter - the name of the method in code-behind to call when a drag moves over this target
DropTarget.Leave - the name of the method in code-behind to call when a drag leaves this target
DropTarget.Drop - the name of the method in code-behind to call when something is dropped on this target
VisualTarget (read only) - the control this behavior is attached to

Imports System.Reflection
Imports System.Runtime.CompilerServices
 
Namespace Behaviors
    Public Enum eDragInitiator
        LeftMouse = 1
        MiddleMouse = 2
        RightMouse = 4
        AnyMouse = 7
    End Enum
 
    Public Enum eErrorHandler
        None
        Console
        Exception
        DialogBox
    End Enum
 
    Public Class DragSource
        Public Property Initiator As eDragInitiator = eDragInitiator.LeftMouse
        Public Property Drop As String
 
        Private _VisualSource As FrameworkElement
        Public Property VisualSource As FrameworkElement
            Get
                Return _VisualSource
            End Get
            Friend Set(value As FrameworkElement)
                _VisualSource = value
            End Set
        End Property
 
        Public Property ErrorHandler As eErrorHandler = eErrorHandler.Console
 
        Friend IsCopy As Boolean
        Friend IsDragging As Boolean = False
        Friend InitiatedBy As MouseButton
        Friend VisualPage As FrameworkElement
        Friend VisualOffsetX As Double
        Friend VisualOffsetY As Double
        Friend HasDragImage As Boolean
 
        Public Sub New()
 
        End Sub
        Public Sub New(UI As FrameworkElement)
            Me.VisualSource = UI
        End Sub
    End Class
 
    Public Class DropTarget
        Public Property Enter As String
        Public Property Leave As String
        Public Property Drop As String
 
        Private _VisualTarget As FrameworkElement
        Public Property VisualTarget As FrameworkElement
            Get
                Return _VisualTarget
            End Get
            Friend Set(value As FrameworkElement)
                _VisualTarget = value
            End Set
        End Property

        Friend Property IsMouseOver As Boolean
 
        Public Sub New()
 
        End Sub
        Public Sub New(UI As FrameworkElement)
            Me.VisualTarget = UI
        End Sub
    End Class
 
‘ Todo – add the behaviors here
End Namespace

Now we can add the behavior class and the attached properties.

ActiveDragSource - there can only ever be one control being dragged. This is it.
ActiveDropTarget - the mouse can only ever be over one drop target. This is it.
DropTargets - a shared collection of potential drop targets.

    Public Class DragDropBehavior
        Inherits DependencyObject
 
        Private Shared ActiveDragSource As DragSource
        Private Shared ActiveDropTarget As DropTarget
        Private Shared DropTargets As New List(Of DropTarget)
 
        Public Shared ReadOnly DragSourceProperty As Windows.DependencyProperty =
        DependencyProperty.RegisterAttached("DragSource", GetType(DragSource), GetType(DragDropBehavior),
                                            New Windows.PropertyMetadata(New DragSource(), AddressOf DragSource_Changed))
 
        Public Shared Function GetDragSource(o As DependencyObject) As Object
            Return o.GetValue(DragSourceProperty)
        End Function
 
        Public Shared Sub SetDragSource(o As DependencyObject, value As Object)
            o.SetValue(DragSourceProperty, value)
        End Sub
 
        Private Shared Sub DragSource_Changed(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
 
            Dim dep As FrameworkElement = TryCast(d, FrameworkElement)
            Dim ds As DragSource = TryCast(e.NewValue, DragSource)
 
            Try
                If dep Is Nothing Then Exit Sub
                If ds Is Nothing Then
                    RemoveDragHandlers(dep, ds)
                Else
                    AddDragHandlers(dep, ds)
                End If
            Catch ex As Exception
                HandleException(ex)
            End Try
        End Sub
 
        Public Shared ReadOnly DropTargetProperty As Windows.DependencyProperty =
        DependencyProperty.RegisterAttached("DropTarget", GetType(DropTarget), GetType(DragDropBehavior),
                                    New PropertyMetadata(New DropTarget(), AddressOf DropTarget_Changed))
 
        Public Shared Function GetDropTarget(o As DependencyObject) As Object
            Return o.GetValue(DropTargetProperty)
        End Function
 
        Public Shared Sub SetDropTarget(o As DependencyObject, value As Object)
            o.SetValue(DropTargetProperty, value)
        End Sub
 
        Private Shared Sub DropTarget_Changed(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
            Dim dep As FrameworkElement = TryCast(d, FrameworkElement)
            Dim dropTarget As DropTarget = TryCast(e.NewValue, DropTarget)
 
            Try
                If dep Is Nothing Then Exit Sub
                If dropTarget Is Nothing Then
                    DropTargets.Remove(dropTarget)
                Else
                    dropTarget.VisualTarget = dep
                    DropTargets.Add(dropTarget)
                End If
            Catch ex As Exception
                HandleException(ex)
            End Try
        End Sub

        Private Shared Sub RemoveDragHandlers(dep As FrameworkElement, dragSource As DragSource)
            RemoveHandler dep.MouseMove, AddressOf MouseMove
            RemoveHandler dep.PreviewMouseUp, AddressOf PreviewMouseUp
        End Sub
 
        Private Shared Sub AddDragHandlers(dep As FrameworkElement, dragSource As DragSource)
            AddHandler dep.MouseMove, AddressOf MouseMove
            AddHandler dep.PreviewMouseUp, AddressOf PreviewMouseUp
        End Sub

' To do - add supporting code here

    End Class

A drag operation starts when the mouse moves over a drag source while a mouse button is down and we are not currently dragging. When it starts we need to create the adorner and note that we are now dragging.

If a mouse move occurs and a mouse button is down and we are already dragging, then we need to continue dragging by moving the adorner and checking which drop target (if any) we are over.

        Private Shared Sub MouseMove(sender As Object, e As MouseEventArgs)
 
            Dim UI As FrameworkElement = TryCast(sender, FrameworkElement)
            Dim ds As DragSource
 
            Try
                If UI Is Nothing Then Exit Sub
 
                ds = GetDragSource(UI)
 
                If ActiveDragSource IsNot Nothing AndAlso ActiveDragSource.IsDragging Then
                    DoDrag(ds)
                Else
                    ConsiderInitiateDrag(UI, ds)
                End If
 
                CheckForMovement(UI)
                UpdateActiveDropTarget()
            Catch ex As Exception
                HandleException(ex)
            End Try
        End Sub
 
        Private Shared Sub ConsiderInitiateDrag(UI As FrameworkElement, ds As DragSource)
            Try
                If (ds.Initiator And eDragInitiator.LeftMouse) AndAlso Mouse.LeftButton = MouseButtonState.Pressed Then
                    ds.InitiatedBy = MouseButton.Left
                    InitiateDrag(UI, ds)
                End If
                If (ds.Initiator And eDragInitiator.MiddleMouse) AndAlso Mouse.MiddleButton = MouseButtonState.Pressed Then
                    ds.InitiatedBy = MouseButton.Middle
                    InitiateDrag(UI, ds)
                End If
                If (ds.Initiator And eDragInitiator.RightMouse) AndAlso Mouse.RightButton = MouseButtonState.Pressed Then
                    ds.InitiatedBy = MouseButton.Right
                    InitiateDrag(UI, ds)
                End If
            Catch ex As Exception
                HandleException(ex)
            End Try
        End Sub
 
        Private Shared Sub InitiateDrag(UI As FrameworkElement, DragSource As DragSource)
 
            Dim ElementPosition As Point
 
            Try
                DragSource = GetDragSource(UI)
                If DragSource.IsDragging Then Exit Sub
 
                DragSource.VisualPage = FindVisualRoot(UI)
                DragSource.VisualSource = UI
                If DragSource.VisualPage IsNot Nothing Then
                    ' If we can find a container, we can add an image of the control to the drag cursor as an adorner
                    ElementPosition = Mouse.GetPosition(DragSource.VisualSource)
                    DragSource.VisualOffsetX = ElementPosition.X
                    DragSource.VisualOffsetY = ElementPosition.Y
 
                    Dim al As AdornerLayer = AdornerLayer.GetAdornerLayer(DragSource.VisualSource)
                    al.Add(New DragDropAdorner(DragSource.VisualSource))
                    DragSource.HasDragImage = True
                Else
                    Console.WriteLine("No container was found - no drag image is available")
                End If
 
                DragSource.IsDragging = True
 
                ActiveDragSource = DragSource
                UI.CaptureMouse()
                DragSource.IsCopy = (Keyboard.IsKeyDown(Key.LeftCtrl))
                DragSource.VisualPage.Cursor = Cursors.Hand
            Catch ex As Exception
                HandleException(ex)
            End Try
        End Sub
 
        Private Shared Sub DoDrag(ds As DragSource)
 
            Try
                If Not ds.HasDragImage Then Exit Sub
                Dim PagePosition As Point = Mouse.GetPosition(ds.VisualPage)
                Dim dda As DragDropAdorner = AdornerLayer.GetAdornerLayer(ActiveDragSource.VisualSource).GetAdorners(ActiveDragSource.VisualSource).FirstOrDefault()
                Dim X As Double = PagePosition.X - ActiveDragSource.VisualOffsetX
                Dim Y As Double = PagePosition.Y - ActiveDragSource.VisualOffsetY
                dda.RenderTransform = New TranslateTransform(X, Y)
            Catch ex As Exception
                HandleException(ex)
            End Try
        End Sub

         Private Shared Sub CheckForMovement(UI As FrameworkElement)
 
            Dim p As Point
 
            Try
                p = Mouse.GetPosition(UI)
                For Each dropTarget As DropTarget In DropTargets
                    If dropTarget.VisualTarget IsNot Nothing Then
                        p = Mouse.GetPosition(dropTarget.VisualTarget)
                        If p.X >= 0 And p.Y >= 0 And p.X <= dropTarget.VisualTarget.ActualWidth And p.Y <= dropTarget.VisualTarget.ActualHeight Then
                            If Not dropTarget.IsMouseOver Then
                                MouseEnter(dropTarget)
                            End If
                        Else
                            If dropTarget.IsMouseOver Then
                                MouseLeave(dropTarget)
                            End If
                        End If
                    End If
                Next
            Catch ex As Exception
                HandleException(ex)
            End Try
        End Sub
 
        Private Shared Sub UpdateActiveDropTarget()
 
            Try
                ActiveDropTarget = DropTargets.FirstOrDefault(Function(dt) dt.IsMouseOver)
                If ActiveDragSource IsNot Nothing AndAlso ActiveDragSource.IsDragging Then
                    ActiveDragSource.VisualPage.Cursor = If(ActiveDropTarget Is Nothing, Cursors.No, Cursors.Hand)
                End If
            Catch ex As Exception
                HandleException(ex)
            End Try
        End Sub

That lot takes care of initiating or continuing a drag operation. Now we need to write the mouse button up event handler.

        Private Shared Sub PreviewMouseUp(sender As Object, e As MouseButtonEventArgs)
            ' A mouse button has been released, was it the initiating button
            Try
                If ActiveDragSource IsNot Nothing AndAlso ActiveDragSource.IsDragging AndAlso e.ChangedButton = ActiveDragSource.InitiatedBy Then
                    DoDrop(sender)
                End If
            Catch ex As Exception
                HandleException(ex)
            End Try
        End Sub
 
        Private Shared Sub DoDrop(sender As Object)
 
            Dim UI As FrameworkElement = TryCast(sender, FrameworkElement)
            Dim MI As MethodInfo
 
            Try
                If UI Is Nothing Then Exit Sub
                If ActiveDragSource Is Nothing OrElse Not ActiveDragSource.IsDragging Then Exit Sub
 
                If ActiveDragSource IsNot Nothing Then
                    If Not String.IsNullOrEmpty(ActiveDragSource.Drop) Then
                        MI = UI.DataContext.GetType().GetMethod(ActiveDragSource.Drop, {GetType(DragSource), GetType(DropTarget)})
                        If MI Is Nothing Then
                            HandleException(New Exception($"Public Sub {ActiveDragSource.Drop}(DragSource, DropTarget) not found in {UI.DataContext.ToString()}"))
                        Else
                            MI.Invoke(UI.DataContext, {ActiveDragSource, ActiveDropTarget})
                        End If
                    End If
                End If
 
                If ActiveDropTarget IsNot Nothing Then
                    If Not String.IsNullOrEmpty(ActiveDropTarget.Drop) Then
                        MI = UI.DataContext.GetType().GetMethod(ActiveDropTarget.Drop, {GetType(DragSource), GetType(DropTarget)})
                        If MI Is Nothing Then
                            HandleException(New Exception($"Public Sub {ActiveDropTarget.Drop}(DragSource, DropTarget) not found in {UI.DataContext.ToString()}"))
                        Else
                            MI.Invoke(UI.DataContext, {ActiveDragSource, ActiveDropTarget})
                        End If
                    End If
                End If
 
                ClearDropTargets()
                ActiveDragSource.VisualPage.Cursor = Cursors.Arrow
                ActiveDragSource.IsDragging = False

                Dim al As AdornerLayer = AdornerLayer.GetAdornerLayer(ActiveDragSource.VisualSource)
                Dim dda As Adorner = al.GetAdorners(ActiveDragSource.VisualSource).FirstOrDefault(Function(a) TypeOf a Is DragDropAdorner)
                al.Remove(dda)
                UI.ReleaseMouseCapture()
            Catch ex As Exception
                HandleException(ex)
            End Try
        End Sub
 
        Private Shared Sub ClearDropTargets()
 
            Try
                If ActiveDropTarget IsNot Nothing Then
                    MouseLeave(ActiveDropTarget)
                    ActiveDropTarget = Nothing
                End If
            Catch ex As Exception
                HandleException(ex)
            End Try
        End Sub

         ' return the UI element's parent page, user control, or window
        Private Shared Function FindVisualRoot(UI As FrameworkElement) As FrameworkElement
 
            Dim Parent As FrameworkElement = UI
            Dim ParentType As Type
            Dim UserControlType As Type = GetType(UserControl)
            Dim WindowType As Type = GetType(Window)
            Dim PageType As Type = GetType(Page)
 
            Try
                Do
                    Parent = VisualTreeHelper.GetParent(Parent)
                    ParentType = Parent.GetType()
                Loop While Not (ParentType.IsSubclassOf(UserControlType) Or ParentType.IsSubclassOf(WindowType) Or ParentType.IsSubclassOf(PageType))
            Catch ex As Exception
                HandleException(ex)
            End Try
            Return Parent
        End Function

 As we move the mouse around, we need to tell drop targets when we enter or leave them.

        Private Shared Sub MouseEnter(ByRef dropTarget As DropTarget)
 
            Try
                If ActiveDragSource Is Nothing OrElse Not ActiveDragSource.IsDragging Then Exit Sub
                If String.IsNullOrEmpty(dropTarget.Enter) Then Exit Sub
 
                Dim UI As FrameworkElement = dropTarget.VisualTarget
                Dim MI As MethodInfo
 
                dropTarget.IsMouseOver = True
 
                If UI Is Nothing Then Exit Sub
                MI = UI.DataContext.GetType().GetMethod(dropTarget.Enter, {GetType(DragSource), GetType(DropTarget)})
                If MI Is Nothing Then
                    HandleException(New Exception($"Public Sub {dropTarget.Enter}(DragSource, DropTarget) not found in {UI.DataContext.ToString()}"))
                Else
                    MI.Invoke(UI.DataContext, {ActiveDragSource, dropTarget})
                End If
            Catch ex As Exception
                HandleException(ex)
            End Try
        End Sub
 
        Private Shared Sub MouseLeave(ByRef dropTarget As DropTarget)
 
            Try
                If ActiveDragSource Is Nothing OrElse Not ActiveDragSource.IsDragging Then Exit Sub
                If String.IsNullOrEmpty(dropTarget.Leave) Then Exit Sub
 
                Dim UI As FrameworkElement = dropTarget.VisualTarget
                Dim MI As MethodInfo
 
                dropTarget.IsMouseOver = False
 
                If UI Is Nothing Then Exit Sub
                MI = UI.DataContext.GetType().GetMethod(dropTarget.Leave, {GetType(DragSource), GetType(DropTarget)})
                If MI Is Nothing Then
                    HandleException(New Exception($"Public Sub {dropTarget.Leave}(DragSource, DropTarget) not found in {UI.DataContext.ToString()}"))
                Else
                    MI.Invoke(UI.DataContext, {ActiveDragSource, dropTarget})
                End If
            Catch ex As Exception
                HandleException(ex)
            End Try
 
        End Sub
 
        Private Shared Function GetDropTarget(UI As FrameworkElement) As DropTarget
 
            Try
                For Each dt As DropTarget In DropTargets
                    If dt.VisualTarget.Equals(UI) Then
                        Return dt
                    End If
                Next
            Catch ex As Exception
                HandleException(ex)
            End Try
            Return Nothing
        End Function

Lastly we need a way to consistently handle errors.

        Private Shared Sub HandleException(ex As Exception, <CallerMemberName> Optional CallingMethod As String = "")

 
            Dim ErrorMessage As String = $"{CallingMethod}:{ex.Message}"
            If ActiveDragSource Is Nothing Then
                Console.WriteLine(ErrorMessage)
            Else
                Select Case ActiveDragSource.ErrorHandler
                    Case eErrorHandler.None
                    Case eErrorHandler.Console
                        Console.WriteLine(ErrorMessage)
                    Case eErrorHandler.Exception
                        Throw New Exception(ErrorMessage)
                    Case eErrorHandler.DialogBox
                        MessageBox.Show(ErrorMessage, "Error", MessageBoxButton.OK, MessageBoxImage.Error)
                End Select
            End If
        End Sub

So how do we use all this? Let's put some weird controls in our XAML. The first and last will be complex control and the middle three will be simple text blocks. All of them can be dragged, but only the text blocks will be drop targets. Modify MainWindow.xaml to look like this.

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:ig="http://schemas.infragistics.com/xaml"
        xmlns:local="clr-namespace:DragControl"
        xmlns:behaviors="clr-namespace:DragControl.Behaviors"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Drag Control" Height="450" Width="800">
    <StackPanel Orientation="Vertical" Width="300">
        <StackPanel Orientation="Horizontal" VerticalAlignment="Top">
            <behaviors:DragDropBehavior.DragSource>
                <behaviors:DragSource Initiator="MiddleMouse" Drop="StackPanel_Drop"/>
            </behaviors:DragDropBehavior.DragSource>
            <TextBlock Text="This is a test" Margin="0,0,10,0" Background="Yellow"/>
            <TextBox Width="60" Height="22" MaxLength="4" Text="abc"/>
            <ComboBox Width="100">
                <ComboBoxItem Content="Logger"/>
                <ComboBoxItem Content="VLogger"/>
                <ComboBoxItem Content="Blogger"/>
            </ComboBox>
        </StackPanel>
        <Border Height="22" HorizontalAlignment="Stretch" BorderBrush="Black" Background="PaleGoldenrod">
            <behaviors:DragDropBehavior.DragSource>
                <behaviors:DragSource Initiator="MiddleMouse" Drop="StackPanel_Drop"/>
            </behaviors:DragDropBehavior.DragSource>
            <behaviors:DragDropBehavior.DropTarget>
                <behaviors:DropTarget Enter="Drag_Enter" Leave="Drag_Leave"/>
            </behaviors:DragDropBehavior.DropTarget>
            <TextBlock Text="Insert 1"/>
        </Border>
        <Border Height="22" HorizontalAlignment="Stretch" BorderBrush="Black">
            <behaviors:DragDropBehavior.DragSource>
                <behaviors:DragSource Initiator="MiddleMouse" Drop="StackPanel_Drop"/>
            </behaviors:DragDropBehavior.DragSource>
            <behaviors:DragDropBehavior.DropTarget>
                <behaviors:DropTarget Enter="Drag_Enter" Leave="Drag_Leave"/>
            </behaviors:DragDropBehavior.DropTarget>
            <TextBlock Text="Insert 2"/>
        </Border>
        <Border Height="22" HorizontalAlignment="Stretch" BorderBrush="Black">
            <behaviors:DragDropBehavior.DragSource>
                <behaviors:DragSource Initiator="MiddleMouse" Drop="StackPanel_Drop"/>
            </behaviors:DragDropBehavior.DragSource>
            <behaviors:DragDropBehavior.DropTarget>
                <behaviors:DropTarget Enter="Drag_Enter" Leave="Drag_Leave"/>
            </behaviors:DragDropBehavior.DropTarget>
            <TextBlock Text="Insert 3"/>
        </Border>
        <StackPanel Orientation="Horizontal" VerticalAlignment="Top">
            <behaviors:DragDropBehavior.DragSource>
                <behaviors:DragSource Initiator="MiddleMouse" Drop="StackPanel_Drop"/>
            </behaviors:DragDropBehavior.DragSource>
            <TextBlock Text="This is a test" Margin="0,0,10,0"/>
            <TextBox Width="60" Height="22" MaxLength="4" Text="xyz"/>
            <ComboBox Width="100">
                <ComboBoxItem Content="Google"/>
                <ComboBoxItem Content="Poodle"/>
                <ComboBoxItem Content="Foodie"/>
            </ComboBox>
        </StackPanel>
    </StackPanel>
</Window>

 

A window of weird controls

Each control is draggable because it has a DragSource behavior attached. This specifies that the middle mouse button will initiate a drag and the method StackPanel_Drop will be called when this control is dropped over a valid drop target.

<behaviors:DragDropBehavior.DragSource>
    <behaviors:DragSource Initiator="MiddleMouse" Drop="StackPanel_Drop"/>
</behaviors:DragDropBehavior.DragSource>
 
The middle three controls are also drop targets. They specify that when a drag source is dragged over them the Drag_Enter method is called and when it is dragged away the Drag_Leave method is called. This allows them to change how they look to give the user visual cues.

<behaviors:DragDropBehavior.DropTarget>
    <behaviors:DropTarget Enter="Drag_Enter" Leave="Drag_Leave"/>
</behaviors:DragDropBehavior.DropTarget>

Now all we have to do is write those three methods. They all have the same signature. Make MainWindow.xaml.vb look like this. Note the Drop method is called with a null dt if the user does not drop over a valid drop target.

Class MainWindow
 
    Public Sub Drag_Enter(ds As Behaviors.DragSource, dt As Behaviors.DropTarget)
        Dim b As Border = TryCast(dt.VisualTarget, Border)
        If b IsNot Nothing Then
            b.BorderThickness = New Thickness(0, 0, 0, 1)
        End If
    End Sub
 
    Public Sub Drag_Leave(ds As Behaviors.DragSource, dt As Behaviors.DropTarget)
        Dim b As Border = TryCast(dt.VisualTarget, Border)
        If b IsNot Nothing Then
            b.BorderThickness = New Thickness(0, 0, 0, 0)
        End If
    End Sub
 
    Public Sub StackPanel_Drop(ds As Behaviors.DragSource, dt As Behaviors.DropTarget)
        Dim b As Border = DirectCast(dt?.VisualTarget, Border)
        Dim tb As TextBlock = DirectCast(b?.Child, TextBlock)
        MessageBox.Show($"{If(ds.IsCopy, "Copied", "Moved")} to {tb?.Text}")
    End Sub
End Class

See what happens when you use the middle mouse button to drag a control over the third control.

Result of a move operation

If the left control button is down when you initiate the drag you get this instead.

Result of a copy operation



No comments:

Post a Comment