Drag and Drop with Silverlight

Shout it kick it on DotNetKicks.com

The Problem

I have been developing with Silverlight for a few months now, and I have really been enjoying myself.  It has been the enabling technology my project and we have been extremely productive in the environment.  Unfortunately, Silverlight is still in version 2.0 and there are some missing capabilities.

One such hole in the framework is "Drag and Drop".  There is no support for it directly.  There are several blog examples on the web, but I have yet to find a fully encapsulated, generic solution to the "Drag and Drop" problem.  For instance, how soon after dragging do you really want to be dragging?  You don’t want to drag immediately, as that will affect normal clicking on an element.  How do you handle drags from one distinct control to another that are not aware of each other?  How do you clue the user in that a given control is droppable?  How do you add animation to cue the user that the item is being dropped?

Introducing DragNDrop

To answer these questions, I created the DragNDrop class.  It is a manager of sorts, and watches a "Drag Source" for the mouse down events.  The "Drag Source" implements an interface and the "Drop Spot" implements a complimentary interface.  This allows for the "Drag Source" and "Drop Spot" to be blissfully unaware of each other.

Usage

The assumption made with this class is that there is some sort of a payload.  This payload is picked up from the "Drag Source" and dropped into the "Drop Spot".  The "Drop Spot" can accept a payload of any types that it implements from any "Drag Source" that implements the complementary interface.  Lets take a look at the interfaces:

public class DragNDrop<PayloadType>
{
    public interface IThumbnailDragSource
    {
        FrameworkElement DragCursor { get; }
        PayloadType Payload { get; }
    }

    public interface IThumbnailDropSpot
    {
        void DragDropEnter();
        void DragDropExit();
        void ThumbnailDroping(PayloadType dataContext, FrameworkElement cursor, Point cursorPosition);
    }
}

For instance, if a "Drag Source" wanted to allow dragging with a string payload, it would implement DragNDrop<string>.IThumbnailDragSource.  Any "Drop Spot" that wants to accept a string payload would implement DragNDrop<string>.IThumbnailDropSpot.  The DragNDrop class is then constructed with an instance of the "Drag Source" and the DragNDrop class handles everything else.

In addition to the payload, the "Drag Source" needs to provide the DragNDrop class with the cursor that will be displayed and dragged across the screen.  This can be anything; an image, a user control, a rectangle, etc.

The "Drop Spot" will be notified when the cursor is entering and exiting its space, so it can react appropriately.  It will also be notified when the payload is dropped with the instance of the cursor and the absolute position.  This is necessary in case the "Drop Spot" wants to animate the drop in any way.

The Demo

I created the Twitter Search Tool to prove out this concept.  The idea is that search for a term and it returns a list of tweets. When you find a tweet that you like, you can drop it on to the second list to keep track of it.  It is not the most functional app in the world, but it illustrates the usage of the DragNDrop class nicely.  Take a moment to try it out.

Now that you have seen it in action, lets take a look at how I use the DragNDrop class.  This application uses a small data structure called Tweet.  It contains all of the information about a given tweet (user name, user image, text, etc).  The Tweet is the payload.

In the Silverlight UI, I created a user control to represent each item in the list called ListItem.  The XAML is available in the full source if you are curious.

public partial class ListItem : UserControl, DragNDrop<Tweet>.IThumbnailDragSource
{
    private readonly DragNDrop<Tweet> _dragNDrop;

    public ListItem()
    {
        InitializeComponent();

        _dragNDrop = new DragNDrop<Tweet>(this);
        _dragNDrop.DraggingEnabledDistance = 5.0;
    }

    public FrameworkElement DragCursor
    {
        get
        {
            return new Image
            {
                Width = TweetImage.ActualWidth,
                Height = TweetImage.ActualHeight,
                Source = TweetImage.Source,
                Opacity = 0.5
            };
        }
    }

    public Tweet Payload
    {
        get { return DataContext as Tweet; }
    }
}

This code is pretty straight-forward.  The DragCursor returns a new image with the picture of the user.  The payload is the actual Tweet.  The constructor creates an instance of DragNDrop and passes itself in as the source.  It also sets the DraggingEnabledDistance.  This is the distance (in pixels) that the user must drag before the dragging really begins.  The default is 10 pixels.

Next, lets take a look at the "Drop Spot" code.  It is also a user control, TweetDropList, which includes a ListBox control.  It also includes an opaque canvas that gives the effect of highlighting when it is made visible.

public partial class TweetDropList : DragNDrop<Tweet>.IThumbnailDropSpot
{
    private readonly ObservableCollection<Tweet> _savedTweets = new ObservableCollection<Tweet>();

    public TweetDropList()
    {
        InitializeComponent();
        DropList.ItemsSource = _savedTweets;
    }

    public void DragDropEnter()
    {
        Highlight.Visibility = Visibility.Visible;
    }

    public void DragDropExit()
    {
        Highlight.Visibility = Visibility.Collapsed;
    }

    public void ThumbnailDroping(Tweet dataContext, FrameworkElement cursor, Point cursorPosition)
    {
        DragDropExit();
        AnimateCursor(cursor, cursorPosition, () => _savedTweets.Add(dataContext));
    }
}

I have left out the AnimateCursor code, but it simply generates a storyboard on the cursor and quickly morphs it down to a size of zero.  That code can be found in the source bundle.  The rest of this implementation is extremely simple.  DragDropEnter and DragDropExit simply hides and shows the highlight layer.  ThumbnailDropping will remove the highlighting layer and animate the cursor to give the effect of the item being dropped.  When the animation completes, the new tweet is added to the _savedTweets collection, which will cause the user control to display the new tweet.

Thats It!

I designed the DragNDrop class to be as simple to use as possible.  I have been using a version of this class in my product and I have had great results.  The "Drop Spots" can implement as many versions of IThumbnailDropSpot as it needs to allow different types of items to be dragged.  The DragNDrop class can be instantiated as many times as necessary, for every "Drag Source". 

Please play with this class and give me feedback.  I’d love to hear how it is being used.

DragNDrop Twitter Demo

DragNDrop Twitter Demo Source

DragNDrop.cs

Extensions.cs (a handful of extension methods that DragNDrop uses)

Leave a Reply