Click here to Skip to main content
Click here to Skip to main content

Dragging an element with the DragSource class in WPF

, 14 Sep 2013
Rate this:
Please Sign up or sign in to vote.
Dragging an element with the DragSource class in WPF.

Introduction

Implementing drag and drop can be done with procedural codes or without any procedural codes by attached property based library called attached behavior. Since setting properties is more easier than writing procedural codes, the latter is more easy to use. This is another attached behavior. The DragSource class provides attached properties for drag operation. Major differences of this class are as follows:

  • It provides extensive attached properties that give you more control of drag operation.
  • It provides auto generated drag image called visual feedback that is based on dragged element.
  • It uses a window to show visual feedback. So it can be shown outside of host application.

This class is a part of the Nicenis project. You can find the latest information from the CodePlex project at https://nicenis.codeplex.com. If you just want a short version tutorial, you can skip this article and check this tip.

Basics

If you write procedural codes to make an element draggable, you have to handle an event such as PreviewMouseDown. Furthermore, you must track the mouse movement to see whether it is a drag gesture. If it is a drag gesture, you also have to call the following DragDrop.DoDragDrop method.

public static DragDropEffects DoDragDrop
(
    // A reference to the dependency object that is the source of the data being dragged.
    DependencyObject dragSource,

    // A data object that contains the data being dragged. 
    Object data,

    // One of the DragDropEffects values that specifies permitted effects of the drag-and-drop operation.
    DragDropEffects allowedEffects
)

In most attached behaviors, these codes are packaged into a attached property changed callback. DragSource provides the AllowDrag attached property for this purpose. When AllowDrag is set to true, it attaches required event handlers.

static void AllowDragProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    UIElement target = d as UIElement;

    // Removes the previous event handler if it exists.
    target.PreviewMouseDown -= AllowDragProperty_PropertyHost_PreviewMouseDown;

    if ((bool)e.NewValue)
        target.PreviewMouseDown += AllowDragProperty_PropertyHost_PreviewMouseDown;
}

For required parameters of the DoDragDrop method, DragSource provides the Data and AllowedEffects attached properties. So you can make a draggable element like the following XAML without any procedural code.

<Window
    x:Class="DragSourceSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
    Title="MainWindow" Height="200" Width="200">

    <!-- This border will be draggable.-->
    <Border
        n:DragSource.AllowDrag="True"
        n:DragSource.Data="Test Data"
        n:DragSource.AllowedEffects="Copy"
        Margin="30"
        Background="Green"
        />
</Window>

AllowedEffects is not a required property so you can omit it. It will look like this image if you drag the border.

Recognizing drag gesture

A drag operation is started by pressing an element and moving it. Pressing an element can be archived by various means such as mouse, touch, etc. It is important to control which can start drag operation. DragSource provides the AllowedInitiators attached property of which type is the DragInitiators enumeration.

[Flags]
public enum DragInitiators
{
    MouseLeftButton = DragInitiator.MouseLeftButton,
    MouseMiddleButton = DragInitiator.MouseMiddleButton,
    MouseRightButton = DragInitiator.MouseRightButton,
    MouseXButton1 = DragInitiator.MouseXButton1,
    MouseXButton2 = DragInitiator.MouseXButton2,

    /// <summary>
    /// All mouse related drag initiators.
    /// </summary>
    Mouse = MouseLeftButton
               | MouseMiddleButton
               | MouseRightButton
               | MouseXButton1
               | MouseXButton2,

    Default = MouseLeftButton,
    All = Mouse,
}
Currently, the only supported device is mouse. The following XAML shows a rectangle that can be dragged by the right mouse button.
<Window
    x:Class="DragSourceSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
    Title="MainWindow" Height="200" Width="200">

    <!-- This border will be draggable.-->
    <Border
        n:DragSource.AllowDrag="True"
        n:DragSource.Data="Test Data"
        n:DragSource.AllowedEffects="Copy"
        n:DragSource.AllowedInitiators="MouseRightButton"
        Margin="30"
        Background="Green"
        />
</Window>
When you start moving an element, it doesn't start a drag operation immediately. There are some movement thresholds which are set by the following attached properties.
  • MinimumHorizontalDragDistance: The width of a rectangle centered on a drag point to allow for limited movement of the pointer before a drag operation begins.
  • MinimumVerticalDragDistance: The height of a rectangle centered on a drag point to allow for limited movement of the pointer before a drag operation begins.
The following image shows how these values are applied.

If you drag the point out of the dotted rectangle, a drag operation is started. The following image shows a movement that starts a drag operation.

There are two relevant coordinates when you try to start a drag operation:

  • ContactPosition: The first coordinate you were pressing.
  • DraggedPosition: The coordinate you were dragging inside the rectangle before starting a drag operation.

These values are provided as read only attached properties. The ContactPosition is especially useful. By default, the position you clicked in dragged element is not preserved when it is dragged. You can set the ContactPosition to the following attached property to solve it.

  • VisualFeedbackOffset: An offset that is pointed by a pointing device in the visual feedback.

The following XAML shows a rectangle that preserves the clicked position when it is dragged.

<Window
    x:Class="DragSourceSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
    Title="MainWindow" Height="200" Width="200">

    <!-- This border will be draggable.-->
    <Border
        n:DragSource.AllowDrag="True"
        n:DragSource.Data="Test Data"
        n:DragSource.AllowedEffects="Copy"
        n:DragSource.VisualFeedbackOffset="{Binding (n:DragSource.ContactPosition), RelativeSource={RelativeSource Self}}"
        Margin="30"
        Background="Green"
        />
</Window>

To see whether it leaves the dotted rectangle, the movement must be monitored. In most cases, handling a move event such as PreviewMouseMove is enough. But if you try to drag near edge of an element, it will not work without capturing device.

Capturing device is not cheap. For example, only one element at a time can capture the mouse. So it is very likely to break other UI logic if a device is captured to monitor movement. DragSource does not capture any device. Instead, it uses a timer to check movement periodically. The timer is enabled when it leaves an element and disabled when it enters an element.

static void AllowDragProperty_PropertyHost_MouseLeave(object sender, MouseEventArgs e)
{
    // Since it is not rely on the capture, MouseMove event can not be used.
    // It is required to check cursor position periodically.
    GetSafeContext(sender as UIElement).ProcessMoveForDragSensingTimer.Start();
}

static void AllowDragProperty_PropertyHost_MouseEnter(object sender, MouseEventArgs e)
{
    // Stops the process move timer.
    GetSafeContext(sender as UIElement).ProcessMoveForDragSensingTimer.Stop();
}

The following is the timer used in DragSource. You can see it checks movement for every 100ms.

DispatcherTimer _processMoveForDragSensingTimer;

public DispatcherTimer ProcessMoveForDragSensingTimer
{
    get
    {
        if (_processMoveForDragSensingTimer == null)
        {
            _processMoveForDragSensingTimer = new DispatcherTimer();
            _processMoveForDragSensingTimer.Interval = TimeSpan.FromMilliseconds(100);
            _processMoveForDragSensingTimer.Tick += (_, __) => ProcessDragSensing(_target, Mouse.GetPosition(_target));
        }

        return _processMoveForDragSensingTimer;
    }
}

If a drag gesture is recognized, a drag operation is started and the following readonly attached property is set to true.

  • IsDragging: Indicates whether the drag is in progress.

It can be used to show that a drag operation is started like the following:

<Window
    x:Class="DragSourceSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
    Title="MainWindow" Height="200" Width="200">

    <!-- This border will be draggable.-->
    <Border
        n:DragSource.AllowDrag="True"
        n:DragSource.Data="Test Data"
        n:DragSource.AllowedEffects="Copy"
        n:DragSource.VisualFeedbackOffset="{Binding (n:DragSource.ContactPosition), RelativeSource={RelativeSource Self}}"
        Margin="30"
        >
        <Border.Style>
            <Style TargetType="Border">
                <Style.Triggers>
                    <!-- If this border is dragging, sets its background to red. -->
                    <Trigger Property="n:DragSource.IsDragging" Value="True">
                        <Setter Property="Background" Value="Red" />
                    </Trigger>
                </Style.Triggers>

                <Setter Property="Background" Value="Green" />
            </Style>
        </Border.Style>
    </Border>
</Window>

Visual feedback

By default, DragSource shows a generated visual feedback while you are dragging an element. The generated visual feedback is created by using the VisualBrush. The following is the method used in DragSource. It returns a rectangle of which Fill is set to a VisualBrush of a dragged element.

private static FrameworkElement CreateGeneratedContent(UIElement dragSource)
{
    // If the drag source is null
    if (dragSource == null)
        return null;

    // Creates a rectangle with a visual brush.
    Rectangle rectangle = new Rectangle()
    {
        StrokeThickness = 0d,
        Fill = new VisualBrush(dragSource),
        HorizontalAlignment = HorizontalAlignment.Left,
        VerticalAlignment = VerticalAlignment.Top,
    };

    // Sets the rectangle's width binding.
    Binding widthBinding = new Binding("ActualWidth");
    widthBinding.Source = dragSource;
    rectangle.SetBinding(Rectangle.WidthProperty, widthBinding);

    // Sets the rectangle's height binding.
    Binding heightBinding = new Binding("ActualHeight");
    heightBinding.Source = dragSource;
    rectangle.SetBinding(Rectangle.HeightProperty, heightBinding);

    // Returns the rectangle.
    return rectangle;
}

The two bindings in the above method are used to size the visual feedback properly. If you need to change the visual feedback size, you can use the following attached properties.

  • VisualFeedbackWidth: The width of the visual feedback.
  • VisualFeedbackHeight: The height of the visual feedback.
  • VisualFeedbackMinWidth: The minimum width of the visual feedback.
  • VisualFeedbackMinHeight: The minimum height of the visual feedback.
  • VisualFeedbackMaxWidth: The maximum width of the visual feedback.
  • VisualFeedbackMaxHeight: The maximum height of the visual feedback.

If you want to customize the visual feedback, you can use the following attached property.

  • VisualFeedback: An object that is set to the content of the visual feedback.

The following XAML uses an ellipse as a custom visual feedback.

<Window
    x:Class="DragSourceSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
    Title="MainWindow" Height="200" Width="200"
    >
    <Border
        n:DragSource.AllowDrag="True"
        n:DragSource.Data="Test Data"
        n:DragSource.VisualFeedbackOffset="70 70"
        Margin="30"
        Background="Green"
        >
        <!-- An ellipsis is set as a visual feedback.-->
        <n:DragSource.VisualFeedback>
            <Ellipse Fill="Red" Width="140" Height="140" />
        </n:DragSource.VisualFeedback>
    </Border>
</Window>

It will look like the following if you drag the border.

Visual feedback inherits data context from dragged element by default. You can override it with the following attached property.

  • VisualFeedbackDataContext: An object that is set to the data context of the visual feedback.

This XAML uses a custom data context for visual feedback.

<Window
    x:Class="DragSourceSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
    Title="MainWindow" Height="200" Width="200"
    >
    <Border
        n:DragSource.AllowDrag="True"
        n:DragSource.Data="Test Data"
        n:DragSource.VisualFeedbackOffset="0 0"
        n:DragSource.VisualFeedbackDataContext="Custom data context"
        Margin="30"
        Background="Green"
        >
        <!-- "Custom data context" is displayed as a visual feedback.-->
        <n:DragSource.VisualFeedback>
            <TextBlock Text="{Binding}" />
        </n:DragSource.VisualFeedback>
    </Border>
</Window>

It is possible to apply a data template to visual feedback by using the following attached properties.

  • VisualFeedbackTemplate: A data template for the content of the visual drag feedback.
  • VisualFeedbackTemplateSelector: A template selector for the content of the visual drag feedback.

This XAML uses a custom data template for visual feedback.

<Window
    x:Class="DragSourceSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
    Title="MainWindow" Height="200" Width="200"
    >
    <Border
        n:DragSource.AllowDrag="True"
        n:DragSource.Data="Test Data"
        n:DragSource.VisualFeedbackOffset="0 0"
        n:DragSource.VisualFeedback="Custom data template"
        Margin="30"
        Background="Green"
        >
        <!-- "Custom data template" is displayed as a visual feedback.-->
        <n:DragSource.VisualFeedbackTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding}" />
            </DataTemplate>
        </n:DragSource.VisualFeedbackTemplate>
    </Border>
</Window>

Visual feedback is hosted in a window. The host window must be transparent to any input. If you try to set the IsHitTestVisible property of the Window class to false, you may know it does not work. The only way is to use the SetWindowLong function with the WS_EX_TRANSPARENT extended window style by P/Invoke. This is the codes exceprted from DragSource. It creates a host window for visual feedback.

// Creates a host window for the visual drag feedback.
Window hostWindow = new Window()
{
    ShowInTaskbar = false,
    Topmost = true,
    IsHitTestVisible = false,
    AllowsTransparency = true,
    WindowStyle = WindowStyle.None,
    Background = Brushes.Transparent,
    SizeToContent = SizeToContent.WidthAndHeight,
    WindowStartupLocation = WindowStartupLocation.Manual,
};

hostWindow.SourceInitialized += (_, __) =>
{
    // Gets the window handle.
    IntPtr hWnd = new WindowInteropHelper(hostWindow).Handle;

    // Gets the host window's long ptr.
    IntPtr windowLongPtr = Win32.GetWindowLong(hWnd, Win32.GWL_EXSTYLE);
    if (windowLongPtr == IntPtr.Zero)
    {
        Trace.WriteLine("DragSource: GetWindowLongPtr has failed. Error code " + Marshal.GetLastWin32Error());
        return;
    }

    // Ors the WS_EX_TRANSPARENT.
    if (IntPtr.Size == 4)
        windowLongPtr = (IntPtr)(windowLongPtr.ToInt32() | Win32.WS_EX_TRANSPARENT);
    else
        windowLongPtr = (IntPtr)(windowLongPtr.ToInt64() | Win32.WS_EX_TRANSPARENT);

    // Clears the last error for checking SetWindowLong error.
    Win32.SetLastError(0);

    // Sets the new long ptr.
    if (Win32.SetWindowLong(hWnd, Win32.GWL_EXSTYLE, windowLongPtr) == IntPtr.Zero)
    {
        int lastWin32Error = Marshal.GetLastWin32Error();
        if (lastWin32Error != 0)
        {
            Trace.WriteLine("DragSource: SetWindowLong has failed. Error code " + lastWin32Error);
            return;
        }
    }

}; // hostWindow.SourceInitialized

Moving host window to follow the mouse cursor is another issue in WPF. The Mouse.GetPosition method can not be used because drag-and-drop operation is in progress. According to the MSDN, the GetCursorPos function must be used by P/Invoke.

BOOL WINAPI GetCursorPos
(
  // A pointer to a POINT structure that receives the screen coordinates of the cursor.
  _Out_  LPPOINT lpPoint
);

It returns a native coordinate that must be converted into a device independent coordinate. The PresentationSource class provides a Matrix for this purpose. The following is the method excerpted from DragSource. It returns a Matrix for the conversion.

private static Matrix GetTransformFromDevice(Window window)
{
    Debug.Assert(window != null);

    // Gets the host window's presentation source.
    PresentationSource windowPresentationSource = PresentationSource.FromVisual(window);
    if (windowPresentationSource == null)
    {
        Trace.WriteLine("PresentationSource.FromVisual has failed in DragSource.");
        return Matrix.Identity;
    }

    // Returns the TransformFromDevice matrix.
    return windowPresentationSource.CompositionTarget.TransformFromDevice;
}

This is the method used in DragSource to position host window.

private void UpdateHostWindowLocation(Point offset)
{
    // Gets the current mouse positon.
    Win32.POINT cursorPosition;
    if (Win32.GetCursorPos(out cursorPosition) == 0)
    {
        Trace.WriteLine("DragSource: GetCursorPos has failed. Error code " + Marshal.GetLastWin32Error());
        return;
    }

    // Gets the mouse position in device independent coordinate.
    Point windowPosition = GetTransformFromDevice(HostWindow).Transform(new Point(cursorPosition.x, cursorPosition.y));

    // Applies the offset.
    windowPosition.X = windowPosition.X - offset.X;
    windowPosition.Y = windowPosition.Y - offset.Y;

    // Updates the host window's location.
    HostWindow.Left = windowPosition.X;
    HostWindow.Top = windowPosition.Y;
}

Lazy data object creation

When you create a data object for a drag operation, you should consider all possible consumers. In most cases, it is expensive to support various data formats. It will be more worse if you have many draggable items. So you should avoid creating it in early stage. The IDataObjectProvider interface is provided for this purpose.

namespace Nicenis.Windows
{
    /// <summary>
    /// Provides a way to get a data object that contains the data being dragged.
    /// </summary>
    public interface IDataObjectProvider
    {
        /// <summary>
        /// Gets a data object that contains the data being dragged.
        /// </summary>
        /// <returns>A data object that contains the data being dragged.</returns>
        object GetDataObject();
    }
}

If you implement it and set the implementation to the Data attached property, the GetDataObject method is called right before a drag operation is started. This is a sample data context that implements IDataObjectProvider.

public class SampleDataContext : IDataObjectProvider
{
    public object GetDataObject()
    {
        return "Test Data";
    }
}

The following shows how to bind it to the Data attached property.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // Initializes the DataContext.
        DataContext = new SampleDataContext();
    }
}
<Window
    x:Class="DragSourceSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:n="clr-namespace:Nicenis.Windows;assembly=Nicenis"
    Title="MainWindow" Height="200" Width="200">

    <!-- The Data is bound to an implementation of the IDataObjectProvider interface.-->
    <Border
        n:DragSource.AllowDrag="True"
        n:DragSource.Data="{Binding}"
        n:DragSource.VisualFeedbackOffset="{Binding (n:DragSource.ContactPosition), RelativeSource={RelativeSource Self}}"
        Margin="30"
        Background="Green"
        />
</Window>

Events

DragSouce raises several routed events with preview events during drag operation. The first is the DragSensing event. It is raised when user starts some action that can initiate a drag operation.

  • DragSensing: The routed event that is raised when drag gesture recognition is in progress.

It is a cancelable event. If you set the Cancel property of the event arguments to true, it prevents drag operation.

private void Border_DragSensing(object sender, DragSourceDragSensingEventArgs e)
{
    // Prevents starting drag operation.
    e.Cancel = true;
}

When a drag gesture is recognized, a Dragging event is raised before starting a drag operation.

  • Dragging: The routed event that is raised when a dragging is about to start.

It is also a cancelable event like DragSensing. The event arguments provides various properties that are similar to DragSource's attached properties. So you can use it to override most values set by DragSource's attached properties. It allows you to create a data object in a Dragging event handler instead of XAML.

private void Border_Dragging(object sender, DragSourceDraggingEventArgs e)
{
    // Sets a data object that contains the data being dragged.
    e.Data = "The Data";
}

If a drag operation is started, GiveFeedback and QueryContinueDrag event are raised periodically.

  • GiveFeedback: The routed event that is raised when the DragDrop.GiveFeedback event is raised.
  • QueryContinueDrag: The routed event that is raised that is raised when the DragDrop.QueryContinueDrag event is raised.

As you can see in the above description, these events are just wrapper events. It just provides additional properties related to DragSource. The following updates VisualFeedback to show the current time.

private void Border_GiveFeedback(object sender, DragSourceGiveFeedbackEventArgs e)
{
    // Sets the visual feedback to the current time.
    e.VisualFeedback = DateTime.Now.ToString();
}

When a drag operation is over, a Dropped event is raised.

  • Dropped: The routed event that is raised when a drag-and-drop operation is finished.

You can use the FinalEffects property of the event arguments to check how a drag operation is finished.

private void Border_Dropped(object sender, DragSourceDroppedEventArgs e)
{
    if (e.FinalEffects == DragDropEffects.Copy)
    {
        // The dragged data is copied.
    }
    
    if (e.FinalEffects == DragDropEffects.Move)
    {
        // The dragged data is moved.
    }   
}

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

About the Author

JO Hyeong-Ryeol
Software Developer
Korea (Republic Of) Korea (Republic Of)
No Biography provided

Comments and Discussions

 
GeneralMy vote of 1 PinmemberMember 103334186-Feb-14 6:48 
GeneralNice and useful code PinmemberPostfix019-Nov-13 23:42 
GeneralRe: Nice and useful code PinmemberJO Hyeong-Ryeol10-Nov-13 4:24 
Thank you for your comment! Smile | :)

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140709.1 | Last Updated 14 Sep 2013
Article Copyright 2013 by JO Hyeong-Ryeol
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid