Introduction
In this article I'll show how to implement drag and drop in WPF. We'll give the user a "ghosted" preview of the drag operation using a custom user control as the mouse cursor moves, and make the everything bindable so we can use MVVM. We'll provide some events so we can change the UI based on whether a drop operation is allowed, and aside from using MVVM Light for some shortcuts with our view model, this example will not rely on any third party libraries or packages.
Background
An app I recently started working on has no buttons for performing logical operations. Instead, everything is accomplished via drag and drop. I was recently asked to put together a technical demo implementing this core UI feature in WPF.
WPF natively supports drag and drop operations - that is, you the developer get some information when a UI element is dragged across the screen and then dropped on another UI element, but from a user's perspective, there's no feedback. If we want some kind of visual cue, (mouse cursor change, ghosted preview, or moving the element itself) we the developers have to add that functionality.
Some of the other articles and examples I've seen provide drag preview operations by using a third party library. In an enterprise environment, we don't always have that luxury.
So In this article, we'll implement a drag and drop gesture with a ghosted preview to add a new user to a list of users. Additionally, we'll animate a visual indicator on the ghosted image to indicate whether or not the drag/drop is allowed. And we'll do it all without relying on a third party library for our drag and drop support.
The code
This article is rather long, so I've grouped it into two sections
- Using the library
- Building the drag and drop helper library
The first thing we need to do is add our preview user control - that is, the control that gets displayed while a drag operation is underway.
Yours can look however you want it to, but for mine, I'd like to have a user icon with a green plus if I can add it, and a red minus if I can't.
Let's add a user control, and give it the following xaml:
<Grid Name="grid">
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Grid.RenderTransform>
<Image Name="imgIndicator" Source="user-icon.png" />
<Rectangle Name="horizontalBar" Height="50" Width="150" Margin="140,191,10,59">
<Rectangle.Fill>
<SolidColorBrush Color="Green" />
</Rectangle.Fill>
</Rectangle>
<Rectangle Name="verticalBar" Height="150" Width="50" Margin="190,140,60,10">
<Rectangle.Fill>
<SolidColorBrush Color="Green" />
</Rectangle.Fill>
</Rectangle>
</Grid>
In the resources for the user control, I'll add two animations:
The first one changes the horizontal and vertical bars to opaque and green when we can drop
<Storyboard x:Key="canDropChanged" FillBehavior="HoldEnd">
<ColorAnimation To="Green" Storyboard.TargetName="horizontalBar" Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)" BeginTime="00:00:00" Duration="00:00:00.3" />
<ColorAnimation To="Green" Storyboard.TargetName="verticalBar" Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)" BeginTime="00:00:00" Duration="00:00:00.3" />
<DoubleAnimation BeginTime="00:00:00" Duration="00:00:00.25" AccelerationRatio=".1" DecelerationRatio=".9" To="1" Storyboard.TargetName="verticalBar" Storyboard.TargetProperty="(Rectangle.Opacity)" />
</Storyboard>
The second changes the opacity of the vertical bar, and color changes the horizontal to red when we can not drop:
<Storyboard x:Key="cannotDropChanged" FillBehavior="HoldEnd">
<ColorAnimation To="Red" Storyboard.TargetName="horizontalBar" Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)" BeginTime="00:00:00" Duration="00:00:00.3" />
<ColorAnimation To="Red" Storyboard.TargetName="verticalBar" Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)" BeginTime="00:00:00" Duration="00:00:00.3" />
<DoubleAnimation BeginTime="00:00:00" Duration="00:00:00.25" AccelerationRatio=".1" DecelerationRatio=".9" To="0" Storyboard.TargetName="verticalBar" Storyboard.TargetProperty="(Rectangle.Opacity)" />
</Storyboard>
You should note that the library I show how to build makes a hard reference to these storyboard names for transitioning between can and cannot drop states. In a future version, I'll probably change this over to a data trigger. Now. arguably, the color change on the vertical is not technially necessary, however, in my tests I think the fade to red on the horizontal bar looks better when it's not competing with the green on the color change.
Now that we've got the control which will serve as our preview, let's head over to the main window.
MainWindow.xaml
Let's start out by laying out the window:
<Window x:Class="DragDropExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dd="clr-namespace:DragDrop;assembly=DragDrop"
xmlns:controls="clr-namespace:DragDropExample"
DataContext="{Binding Source={StaticResource Locator}, Path=Main}"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<ResourceDictionary>
<controls:DragPreview x:Key="DragPreviewControl" />
</ResourceDictionary>
</Window.Resources>
<Grid>
<Canvas ZIndex="5" x:Name="OverlayCanvas" />
<Grid></Grid>
</Grid>
</Window>
In the library, our preview control is rendered on a Canvas
that we specify at binding time - the overlay canvas will be that canvas. We use XAML's layout to ensure that the canvas covers the entire window, and set the z-index to make sure that anything we put on the canvas is rendered above our other controls.
Let's scroll on down to the <Border> element, and take a look at how we set up the drag source:
<Border BorderBrush="Black" BorderThickness="1" Margin="0 10 0 0"
dd:DragDrop.IsDragSource="True"
dd:DragDrop.DropTarget="{Binding ElementName=dropPanel}"
dd:DragDrop.DragDropPreviewControl="{StaticResource DragPreviewControl}"
dd:DragDrop.DragDropPreviewControlDataContext="{Binding Source={StaticResource Locator},Path=DragPreview}"
dd:DragDrop.DragDropContainer="{Binding ElementName=OverlayCanvas}"
dd:DragDrop.ItemDropped="{Binding AddUser}"
>
All of the attached properties we declared in our DragDrop class are set up here. The item which you set as the drag source will be the item that originates the drag. For purposes of this article, I set the border element to be the drag source, but you can set it to be any control.
- The
DropTarget
is the control that recives the drop operation - there's nothing that we have to do to that control - The
DragDropPreviewControl
is our user control that gets displayed while a drag operation is underway - The
DragDropPrewviewControlDataContext
is the (optional) data context that gets assigned to to the preview control for databinding purposes while a drag operation is underway. If you omit this property, then DragDropPreviewControl
gets this control's data context as its data context. - The
DragDropContainer
is the canvas which will render our preview control during the drag operation - The
ItemDropped
command is the command which will determine if we are allowed to drop, as well as serve as the event handler for when an item is dropped.
Let's now come over and build the library which provides the functionality.
Let's start by setting up some infrastructure first.
DropState.cs
This is a quick enum to tell us what we can and can't do in terms of our drop operation
public enum DropState
{
CanDrop,
CannotDrop
}
DropState.CanDrop
means that the drag preview is inside the droppable area DropState.CannotDrop
means that the drag preview is outside the droppable area
DropEventArgs.cs
When we initiate a drop operation, we'll need some event args to pass to the caller. This is merely a quick wrapper around the data context so we can pass it as an event args object.
public class DragDropEventArgs : EventArgs
{
public Object DataContext;
public DragDropEventArgs() { }
public DragDropEventArgs(Object dataContext)
{
DataContext = dataContext;
}
}
DragDropPreviewBase.cs
DragDropPreviewBase
serves as the base class for what we're going to show to the user while a drag operation is underway. Let's start by declaring the class, and adding some attributes to the render transform so that we have the ability to animate it when the control is loaded or disposed.
public class DragDropPreviewBase : UserControl
{
public DragDropPreviewBase()
{
ScaleTransform scale = new ScaleTransform(1f, 1f);
SkewTransform skew = new SkewTransform(0f, 0f);
RotateTransform rotate = new RotateTransform(0f);
TranslateTransform trans = new TranslateTransform(0f, 0f);
TransformGroup transGroup = new TransformGroup();
transGroup.Children.Add(scale);
transGroup.Children.Add(skew);
transGroup.Children.Add(rotate);
transGroup.Children.Add(trans);
this.RenderTransform = transGroup;
}
}
Next, we'll add a dependency property to this control which allows us to bind the DropState of this control to the data context
#region DropState Dependency Property
#region Binding Property
public DropState DropState
{
get { return (DropState)GetValue(DropStateProperty); }
set { SetValue(DropStateProperty, value); }
}
#endregion
#region Dependency Property
public static readonly DependencyProperty DropStateProperty =
DependencyProperty.Register("DropState", typeof(DropState), typeof(DragDropPreviewBase), new UIPropertyMetadata(DropStateChanged));
public static void DropStateChanged(DependencyObject element, DependencyPropertyChangedEventArgs e)
{
var instance = (DragDropPreviewBase)element;
instance.StateChangedHandler(element, e);
}
public virtual void StateChangedHandler(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ }
#endregion
#endregion
This is pretty standard boilerplate code for a dependency property, so I'll continue on.
DragDrop.cs
Here's where we start to get going. DragDrop
is what we'll use to indicate an object can be a drag source, move the DragDropPreviewBase
, and invoke the ItemDropped
command when the user releases the mouse over the eligible drop target.
Let's start off by declaring the class, and adding two utility methods we'll be using later on:
#region Utilities
public static FrameworkElement FindAncestor(Type ancestorType, Visual visual)
{
while (visual != null && !ancestorType.IsInstanceOfType(visual))
{
visual = (Visual)VisualTreeHelper.GetParent(visual);
}
return visual as FrameworkElement;
}
public static Boolean IsMovementBigEnough(Point initialMousePosition, Point currentPosition)
{
return (Math.Abs(currentPosition.X - initialMousePosition.X) >= SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(currentPosition.Y - initialMousePosition.Y) >= SystemParameters.MinimumVerticalDragDistance);
}
#endregion
FindAncestor
does pretty much what you'd think it does; recursively walks the visual tree, and finds the ancesctor that's of the target type IsMovementBigEnough
determines if the delta between two points is larger than the system defined minimum. We'll need this later on to determine if it's necessary to actually proceed with a drag operation - we don't want to go through the hassle of performing the move if the user is only requesting a drag of 2 pixels (for example)
Next, we'll add the following attached properties. Since these are all implemented using the standard code snippet for attached properties, I'm going to omit pasting the code for these properties in the article.
DropTarget
- The element which will recieve the drop DragDropPreviewControlDataContext
- the data context to associate with the drag and drop operation DragDropPreviewControl
- The DragDropPreviewBase user control which will be displayed as we move the mouse DragDropContainer
- The canvas which we use to absolutely position the preview control ItemDropped
- The ICommand
which will execute when the control is dropped.
We need to add one more attached property, but before we do, we need to set up the instance variables. These values will allow us to capture the values of the attached properties, and store them for later use in the mouse down, mouse move, and mouse up events. When we move the mouse, we want all the values tied to an instance associated with that particular movement, and not accidentally reused if we initiate another drag and drop operation.
private Window _topWindow;
private Point _initialMousePosition;
private Panel _dragDropContainer;
private UIElement _dropTarget;
private Boolean _mouseCaptured;
private DragDropPreviewBase _dragDropPreviewControl;
private Object _dragDropPreviewControlDataContext;
private ICommand _itemDroppedCommand;
private Point _delta;
#region Instance
private static readonly Lazy<DragDrop> _Instance = new Lazy<DragDrop>(() => new DragDrop());
private static DragDrop Instance
{
get { return _Instance.Value; }
}
#endregion
Now, let's add the IsDragSource
attached property
First, let's get the dependency property added,
#region IsDragSource Attached Property
#region Backing Dependency Property
public static readonly DependencyProperty IsDragSourceProperty = DependencyProperty.RegisterAttached(
"IsDragSource", typeof(Boolean), typeof(DragDrop), new PropertyMetadata(false, IsDragSourceChanged));
#endregion
The getter and setter
public static Boolean GetIsDragSource(DependencyObject element)
{
return (Boolean)element.GetValue(IsDragSourceProperty);
}
public static void SetIsDragSource(DependencyObject element, Boolean value)
{
element.SetValue(IsDragSourceProperty, value);
}
And the changed handler
private static void IsDragSourceChanged(DependencyObject element, DependencyPropertyChangedEventArgs e)
{
var dragSource = element as UIElement;
if (dragSource == null)
{ return; }
if (Object.Equals(e.NewValue, true))
{
dragSource.PreviewMouseLeftButtonDown += Instance.DragSource_PreviewMouseLeftButtonDown;
dragSource.PreviewMouseLeftButtonUp += Instance.DragSource_PreviewMouseLeftButtonUp;
dragSource.PreviewMouseMove += Instance.DragSource_PreviewMouseMove;
}
else
{
dragSource.PreviewMouseLeftButtonDown -= Instance.DragSource_PreviewMouseLeftButtonDown;
dragSource.PreviewMouseLeftButtonUp -= Instance.DragSource_PreviewMouseLeftButtonUp;
dragSource.PreviewMouseMove -= Instance.DragSource_PreviewMouseMove;
}
}
Basically, If the dependency property changes to true, we'll wire up the event handlers, and if it changes to false, we uncouple them.
let's start by looking at what happens when we mouse down.
private void DragSource_PreviewMouseLeftButtonDown(Object sender, MouseButtonEventArgs e)
{
try
{
var visual = e.OriginalSource as Visual;
_topWindow = (Window)DragDrop.FindAncestor(typeof(Window), visual);
_initialMousePosition = e.GetPosition(_topWindow);
_dragDropContainer = DragDrop.GetDragDropContainer(sender as DependencyObject) as Canvas;
if (_dragDropContainer == null)
{
_dragDropContainer = (Canvas)DragDrop.FindAncestor(typeof(Canvas), visual);
}
_dropTarget = GetDropTarget(sender as DependencyObject);
_dragDropPreviewControlDataContext = DragDrop.GetDragDropPreviewControlDataContext(sender as DependencyObject);
if (_dragDropPreviewControlDataContext == null)
{ _dragDropPreviewControlDataContext = (sender as FrameworkElement).DataContext; }
_itemDroppedCommand = DragDrop.GetItemDropped(sender as DependencyObject);
}
catch (Exception exc)
{
Console.WriteLine("Exception in DragDropHelper: " + exc.InnerException.ToString());
}
}
What we're doing here is getting our references to the objects we referenced using the dependency properties, and tying them to the member variables of the current instance. It's not much, but it sets us up for the mouse move event.
private void DragSource_PreviewMouseMove(Object sender, MouseEventArgs e)
{
if (_mouseCaptured || _dragDropPreviewControlDataContext == null)
{
return;
}
if (DragDrop.IsMovementBigEnough(_initialMousePosition, e.GetPosition(_topWindow)) == false)
{
return;
}
_dragDropPreviewControl = (DragDropPreviewBase)GetDragDropPreviewControl(sender as DependencyObject);
_dragDropPreviewControl.DataContext = _dragDropPreviewControlDataContext;
_dragDropPreviewControl.Opacity = 0.7;
_dragDropContainer.Children.Add(_dragDropPreviewControl);
_mouseCaptured = Mouse.Capture(_dragDropPreviewControl);
Mouse.OverrideCursor = Cursors.Hand;
Canvas.SetLeft(_dragDropPreviewControl, _initialMousePosition.X - 20);
Canvas.SetTop(_dragDropPreviewControl, _initialMousePosition.Y - 15);
_dragDropContainer.PreviewMouseMove += DragDropContainer_PreviewMouseMove;
_dragDropContainer.PreviewMouseUp += DragDropContainer_PreviewMouseUp;
}
So this is where it gets interesting - when we move the mouse cursor by a significant amount, we'll get the preview control, attach its desired datacontext for MVVM, set its opacity to 70% so it appears that it's being "ghosted"; but once that's done, this future mouse movement events generated by the drag source will bail out early by design.
Even though the mouse movement event is still being originated by the drag source, we're now moving the ghosted preview over the canvas; so it's really more appropriate that the canvas should be handling the preview mouse move event and not the drag source. So, we'll capture that tunneled event calling Mouse.Capture on the movent of the preview control. We also need to know when to stop handling it, so we'll attach a handler to the preview mouse up event as well.
There's a lot happening in the DragDropContainer_PreviewMouseMove
event, so let's step through it.
private void DragDropContainer_PreviewMouseMove(Object sender, MouseEventArgs e)
{
var currentPoint = e.GetPosition(_topWindow);
Mouse.OverrideCursor = Cursors.Hand;
currentPoint.X = currentPoint.X - 20;
currentPoint.Y = currentPoint.Y - 15;
_delta = new Point(_initialMousePosition.X - currentPoint.X, _initialMousePosition.Y - currentPoint.Y);
var target = new Point(_initialMousePosition.X - _delta.X, _initialMousePosition.Y - _delta.Y);
Canvas.SetLeft(_dragDropPreviewControl, target.X);
Canvas.SetTop(_dragDropPreviewControl, target.Y);
_dragDropPreviewControl.DropState = DropState.CannotDrop;
if (_dropTarget == null)
{
AnimateDropState();
return;
}
so first thing we do is get the X and Y of where our mouse is currently located, and then shift it up and to the left just a bit so that the top left corner appears underneath the mouse. Then, physically move it on the canvas by using the Canvas.SetLeft
and Canvas.SetTop
attached properties. Now that we've moved it, if the configuration doesn't give us anywhere to drop it, then animate the drop state to a "cannotDrop" animation, and return.
Next, we'll determine if our mouse is over the drop target.
var transform = _dropTarget.TransformToVisual(_dragDropContainer);
var dropBoundingBox = transform.TransformBounds(new Rect(0, 0, _dropTarget.RenderSize.Width, _dropTarget.RenderSize.Height));
if (e.GetPosition(_dragDropContainer).X > dropBoundingBox.Left &&
e.GetPosition(_dragDropContainer).X < dropBoundingBox.Right &&
e.GetPosition(_dragDropContainer).Y > dropBoundingBox.Top &&
e.GetPosition(_dragDropContainer).Y < dropBoundingBox.Bottom)
{
_dragDropPreviewControl.DropState = DropState.CanDrop;
}
WPF's coordinate system for left, right top and bottom of a given control are relative to the container control. Since the drop target might (and most likely is not) using the same relative coordinates as our pointer, we need to normalize these to the same coordinate system. We do that by calling TransformToVisual.
Now that the coordinate system is normalized, we draw a bounding box, and determine if the pointer is currently located inside the box. If it is, then we'll set the drop state to CanDrop
.
One last thing - Even though we're now physically holding the mouse over the drop target, we still might not be allowed to drop it - that's where our commanding comes in.
if (_itemDroppedCommand != null && _itemDroppedCommand.CanExecute(_dragDropPreviewControlDataContext) == false)
{
_dragDropPreviewControl.DropState = DropState.CannotDrop;
}
If commanding says we can't drop, then we set the drop state to CannotDrop
. Commanding trumps visual.
When we release the mouse, there's some things we need to do:
private void DragDropContainer_PreviewMouseUp(Object sender, MouseEventArgs e)
{
switch (_dragDropPreviewControl.DropState)
{
case DropState.CanDrop:
try
{
var canDropSb = new Storyboard() { FillBehavior = FillBehavior.Stop };
canDropSb.Children.Add(scaleXAnim);
canDropSb.Children.Add(scaleYAnim);
canDropSb.Children.Add(opacityAnim);
canDropSb.Completed += (s, args) => { FinalizePreviewControlMouseUp(); };
canDropSb.Begin(_dragDropPreviewControl);
if (_itemDroppedCommand != null)
{ _itemDroppedCommand.Execute(_dragDropPreviewControlDataContext); }
}
catch (Exception ex)
{ }
break;
case DropState.CannotDrop:
try
{
var cannotDropSb = new Storyboard() { FillBehavior = FillBehavior.Stop };
cannotDropSb.Children.Add(translateXAnim);
cannotDropSb.Children.Add(translateYAnim);
cannotDropSb.Children.Add(opacityAnim);
cannotDropSb.Completed += (s, args) => { FinalizePreviewControlMouseUp(); };
cannotDropSb.Begin(_dragDropPreviewControl);
}
catch (Exception ex) { }
break;
}
_dragDropPreviewControlDataContext = null;
_mouseCaptured = false;
}
Depending on the control's drop state, do a little animation, and then call FinalizePreviewControlMouseUp
, which removes the preview from the canvas, uncouples the handlers, and returns the cursor to normal.
There's one thing left, and that's to handle the preview mouse up event on the control that initiated the drag:
private void DragSource_PreviewMouseLeftButtonUp(Object sender, MouseButtonEventArgs e)
{
_dragDropPreviewControlDataContext = null;
_mouseCaptured = false;
if (_dragDropPreviewControl != null)
{ _dragDropPreviewControl.ReleaseMouseCapture(); }
}
We need to release the mouse capture on the drag preview control so it no longer signals the mouse events.
Credit
Lee Roth for the idea of using a canvas to position user controls
History
2015-03-03 : Initial post