Drag And Drop In Silverlight

Unfortunately Silverlight does not include any direct API support for drag and drop like WPF. In some respects this is not a bad thing as Silverlight does provide all of the building blocks required to perform drag and drop without leading you down any particular implementation, however, sometimes you just want to get going quickly using something that feels familiar.

To that end I’ve been working on a drag drop framework that is largely compatibility with the WPF implementation, I'll attempt to explain the different parts of the implementation and how they fit together in this blog. For the impatient the source code is available here with samples :).

One thing to keep in mind is that the snippets posted in the blog have generally been simplified to get the point across, you will need to refer to the source for the full implementation.

Tracking the Mouse

To track the mouse during a drag operation you need to capture the mouse by calling UIElement.CaptureMouse. Capturing the mouse ensures that all mouse events are sent to an element regardless of whether the mouse is within the elements bounds. The documentation for CaptureMouse states that the mouse may only be captured if the primary mouse button is down, therefore, all drag operations will need to be initiated from a MouseLeftButtonDown event handler, but this is probably what we want, right?

In keeping with our theme of WPF compatibility the API will need to look something like this:

private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
DragDrop.DoDragDrop(dragSource, data, DragDropEffects.Copy);
}

Which UIElement should our implementation of DragDrop use to track the mouse? Many examples of drag and drop use Application.Current.RootVisual to track the mouse and this would probably work just fine, however, previous work I had done on creating a resizable child window led me away from this solution (Don’t worry, I’ll be blogging about the resizable window soon). We really want an element which can be used as an accurate reference point that we have complete control over, and that is unlikely to be affected by layout changes, etc. Popup to the rescue!!

var mouseTracker = new Popup();
if (!mouseTracker.CaptureMouse())
return false;

mouseTracker.MouseMove += MouseMove;
mouseTracker.MouseLeftButtonUp += MouseLeftButtonUp;
mouseTracker.LostMouseCapture += LostMouseCapture;

Dragging Over

At this point we have managed to initiate a drag operation and we are receiving mouse events though the Popup control, but how do we determine which elements we are dragging over and allow them to participate in the drag operation?

To determine the elements currently under the mouse we can use VisualTreeHelper.FindElementsInHostCoordinates, this method returns an IEnumerable<UIElement> containing all of the elements that contain a specified Point in front to back z-order.

Unfortunately allowing the element to participate in the drag operation is slightly more tricky :( First we define an interface for handling drag drop related events (bear with me, I know what you’re thinking…)

public interface IDropTarget
{
void DragLeave(DragEventArgs e);
void DragEnter(DragEventArgs e);
void DragOver(DragEventArgs e);
void Drop(DragEventArgs e);
object Target
{
get;
}
}

Yes, it would be a bit of a pain if we had to inherit from every control we wanted to support drop events and implement IDropTarget. To overcome this we use another common pattern, the attached behaviour. An attached behaviour is an object that attaches itself (usually via an attached property) to an element and adds some new behaviour to that element.

The implementation of DragDropBehaviour uses an attached property to store an instance of itself against an element.

private static readonly DependencyProperty DragDropBehaviourProperty = 
DependencyProperty.RegisterAttached("DragDropBehaviour", typeof(DragDropBehaviour),
typeof(DragDropBehaviour), new PropertyMetadata(null));

It exposes public GetBehaviour and private SetBehaviour static methods for accessing the value of this property. It also exposes a GetOrCreateBehaviour which can be used when a behaviour instance is required.
public static DragDropBehaviour GetOrCreateBehaviour(DependencyObject target)
{
var behaviour = GetBehaviour(target);
if (behaviour == null)
{
behaviour = new DragDropBehaviour(target);
SetBehaviour(target, behaviour);
}
return behaviour;
}

The behaviour implements the IDropTarget interface delegates calls to events declared on the behaviour.

public event DragEventHandler Drop;
public event DragEventHandler DragOver;
public event DragEventHandler DragEnter;
public event DragEventHandler DragLeave;

Ok, this is all very nice but how does it help? Well if we go back to WPF we will notice that the DragDrop class defines several methods for adding and removing drag drop event handlers.

public static void AddDragOverHandler(DependencyObject element, DragEventHandler handler)
public static void AddDragEnterHandler(DependencyObject element, DragEventHandler handler)
public static void AddDragLeaveHandler(DependencyObject element, DragEventHandler handler)
public static void AddDropHandler(DependencyObject element, DragEventHandler handler)
public static void RemoveDragOverHandler(DependencyObject element, DragEventHandler handler)
public static void RemoveDragEnterHandler(DependencyObject element, DragEventHandler handler)
public static void RemoveDragLeaveHandler(DependencyObject element, DragEventHandler handler)
public static void RemoveDropHandler(DependencyObject element, DragEventHandler handler)

With our attached behaviour in place it is a now a simple task to support these methods.

public static void AddDragOverHandler(DependencyObject element, DragEventHandler handler)
{
if (handler != null)
DragDropBehaviour.GetOrCreateAdapter(element).DragOver += handler;
}

Supporting the AllowDrop property

In WPF an element will only receive drag drop related events if its AllowDrop property is set to true. In WPF the AllowDrop property is inherited meaning that child elements that do not explicitly set this property will inherit the value from their parent. Unfortunately Silverlight does not support inherited properties :(

To overcome this limitation and keep code compatibility with WPF we are going to need some trickery. First we define an enum with the values True, False and Inherited, instead of a bool to represent our AllowDrop value.

public enum AllowDrop { Inherited, False, True}
 
We then define an attached dependency property called AllowDrop (surprisingly) whose type is our enum type and set the default value to Parent indicating the value is inherited by default.
 
public static readonly DependencyProperty AllowDropProperty = 
DependencyProperty.RegisterAttached("AllowDrop", typeof (AllowDrop),
typeof (DragDrop), new PropertyMetadata(AllowDrop.Parent));
We then define a utility method that can return the “real” value of the AllowDrop property taking into account inheritance.
 

private static bool AllowsDrop(DependencyObject element)
{
while (element != null)
{
var result = GetAllowDrop(element);
if (result != AllowDrop.Inherited)
{
return result == AllowDrop.True;
}
element = VisualTreeHelper.GetParent(element);
}
return false;
}


Putting it all together

 
Ok, so we now have the ability to track the mouse, determine which elements we are dragging over and query them to determine if we should forward drag drop related events, the only thing left to mention is the mechanism used to publish those events.
 
As most of you will be aware there is no direct support for declaring your own routed events in Silverlight, however, in our case this is not too much of an issue.
private void BubbleDragEvent(DragEventArgs args, Action<IDropTarget, DragEventArgs> handler)
{
var source = _currentDropTarget;
while (source != null && GetAllowDrop(source) != AllowDrop.False)
{
var dropTarget = source.GetDropTarget();
if (dropTarget != null)
{
handler(dropTarget, args);
if (args.Handled)
return;
}
source = VisualTreeHelper.GetParent(source) as UIElement;
}
}


private void OnDrop()
{
BubbleDragEvent((x, y) => x.Drop(y));
}

 

Conclusion

There is a lot more to the framework like Drag Cursors, Cancellation, etc. but I don’t want to bore you too much…, hopefully you’ve got a feel for how things work and the source code should give you the rest. You will notice that the implementation of DragDrop.DoDragDrop has additional parameters not present in WPF, I’m working on removing these by providing support for a default drag cursors and the option to explicitly set the drag cursor for an element with an attached property. Another thing some of you will notice is the lack of tests, within the project I am running we generally follow a TDD approach. This has proved quite difficult for drag drop,  I’m currently looking at using http://www.artoftest.com WebAii to help me here, I’ll keep you posted on how I get on.

4 Comments Filed Under [ Silverlight ]

Comments

# Drag-and-Drop MVVM
Gravatar Drag-and-Drop MVVM
Left by Jason Young's Blog on 7/21/2009 10:37 PM
# re: Drag And Drop In Silverlight
Gravatar This looks good. Thanks for publishing it under an Open Source license. We will seriously look at this for our next version of the Open Source project, http://SilverlightDesktop.net.

We really like "drop effects" parameter in the constructor used to initiate the drag and drop.
Left by Michael Washington on 7/22/2009 1:20 AM
# re: Drag And Drop In Silverlight
Gravatar that's a good sample, i wonder if you can give more details about DragOver event, if I have 3 objects how can i through an event tell that object1 is now over object2 but still have not droped yet.

hope my question is clear.
Left by Tamer Ibrahim on 4/11/2010 4:36 PM
# re: Drag And Drop In Silverlight
Gravatar I'd use DragEnter DragLeave to determine which item you are currently over. You can inspect the Source property of the DragEventArgs to determine the element that triggered the event.
Left by jyoung on 4/13/2010 9:47 PM

Leave Your Comment

Title*
Name*
Email (never displayed)
 (will show your gravatar)
Url
Comment*

Please add 8 and 8 and type the answer here:

Preview Your Comment.