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 |
Inherits Adorner
End Sub
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
Namespace Behaviors
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
Private _VisualSource As FrameworkElement
Public Property VisualSource As FrameworkElement
Get
Return _VisualSource
Friend Set(value As FrameworkElement)
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)
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
Friend Set(value As FrameworkElement)
End Set
End Property
Friend Property IsMouseOver As Boolean
Public Sub New()
End Sub
Public Sub New(UI As FrameworkElement)
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.
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 =
Public Shared Function GetDragSource(o As DependencyObject) As Object
Return o.GetValue(DragSourceProperty)
Public Shared Sub SetDragSource(o As DependencyObject, value As Object)
End Sub
Private Shared Sub DragSource_Changed(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim dep As FrameworkElement = TryCast(d, FrameworkElement)
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 =
Public Shared Function GetDropTarget(o As DependencyObject) As Object
Return o.GetValue(DropTargetProperty)
Public Shared Sub SetDropTarget(o As DependencyObject, value As Object)
End Sub
Private Shared Sub DropTarget_Changed(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
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 AddDragHandlers(dep As FrameworkElement, dragSource As DragSource)
Dim UI As FrameworkElement = TryCast(sender, FrameworkElement)
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)
If (ds.Initiator And eDragInitiator.LeftMouse) AndAlso Mouse.LeftButton = MouseButtonState.Pressed Then
ds.InitiatedBy = MouseButton.Left
End If
If (ds.Initiator And eDragInitiator.MiddleMouse) AndAlso Mouse.MiddleButton = MouseButtonState.Pressed Then
ds.InitiatedBy = MouseButton.Middle
End If
If (ds.Initiator And eDragInitiator.RightMouse) AndAlso Mouse.RightButton = MouseButtonState.Pressed Then
ds.InitiatedBy = MouseButton.Right
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.VisualOffsetY = ElementPosition.Y
Dim al As AdornerLayer = AdornerLayer.GetAdornerLayer(DragSource.VisualSource)
Else
Console.WriteLine("No container was found - no drag image is available")
DragSource.IsDragging = True
ActiveDragSource = DragSource
UI.CaptureMouse()
DragSource.IsCopy = (Keyboard.IsKeyDown(Key.LeftCtrl))
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)
HandleException(ex)
End Try
End Sub
Dim p As Point
Try
p = Mouse.GetPosition(UI)
p = Mouse.GetPosition(dropTarget.VisualTarget)
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)
ActiveDragSource.VisualPage.Cursor = If(ActiveDropTarget Is Nothing, Cursors.No, Cursors.Hand)
Catch ex As Exception
HandleException(ex)
End Try
End Sub
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)
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)})
HandleException(New Exception($"Public Sub {ActiveDragSource.Drop}(DragSource, DropTarget) not found in {UI.DataContext.ToString()}"))
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)})
HandleException(New Exception($"Public Sub {ActiveDropTarget.Drop}(DragSource, DropTarget) not found in {UI.DataContext.ToString()}"))
MI.Invoke(UI.DataContext, {ActiveDragSource, ActiveDropTarget})
End If
End If
End If
ClearDropTargets()
ActiveDragSource.VisualPage.Cursor = Cursors.Arrow
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
Private Shared Function FindVisualRoot(UI As FrameworkElement) As FrameworkElement
Dim Parent As FrameworkElement = UI
Dim UserControlType As Type = GetType(UserControl)
Try
Do
Parent = VisualTreeHelper.GetParent(Parent)
Loop While Not (ParentType.IsSubclassOf(UserControlType) Or ParentType.IsSubclassOf(WindowType) Or ParentType.IsSubclassOf(PageType))
HandleException(ex)
End Try
Return Parent
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
dropTarget.IsMouseOver = True
If UI Is Nothing Then Exit Sub
MI = UI.DataContext.GetType().GetMethod(dropTarget.Enter, {GetType(DragSource), GetType(DropTarget)})
HandleException(New Exception($"Public Sub {dropTarget.Enter}(DragSource, DropTarget) not found in {UI.DataContext.ToString()}"))
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
dropTarget.IsMouseOver = False
If UI Is Nothing Then Exit Sub
MI = UI.DataContext.GetType().GetMethod(dropTarget.Leave, {GetType(DragSource), GetType(DropTarget)})
HandleException(New Exception($"Public Sub {dropTarget.Leave}(DragSource, DropTarget) not found in {UI.DataContext.ToString()}"))
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
Return dt
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)
Select Case ActiveDragSource.ErrorHandler
End If
End Sub
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 |
<behaviors:DragSource Initiator="MiddleMouse" Drop="StackPanel_Drop"/>
</behaviors:DragDropBehavior.DragSource>
<behaviors:DropTarget Enter="Drag_Enter" Leave="Drag_Leave"/>
</behaviors:DragDropBehavior.DropTarget>
b.BorderThickness = New Thickness(0, 0, 0, 1)
End Sub
b.BorderThickness = New Thickness(0, 0, 0, 0)
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 |
No comments:
Post a Comment