Silverlight Drag and Drop Manager

I wrote some classes for handling drag and drop operations in Silverlight that I use within Perenthia. I thought I would go ahead and share them since I found them to be very useful in getting drag and drag working quickly in new projects.

The Drag and Drag classes consist of two interfaces (IDroppable, IDropContainer) and a manager class (DragDropManager). The interfaces are defined as such:

namespace Lionsguard
{
    /// <summary>
    /// Defines an object that can be dragged and dropped via the DragDropManager.
    /// </summary>
	public interface IDroppable
	{
		/// <summary>
		/// An event that is raised when the IDroppable item begins the drag operation.
		/// </summary>
		event BeginDragEventHandler BeginDrag;

		/// <summary>
		/// Gets the UIElement that will server as the cursor replacement when dragging the current object.
		/// </summary>
		/// <returns></returns>
		UIElement GetDragCursor();
	}

    /// <summary>
    /// Defines an object that accept IDroppable items as part of a drop operation.
    /// </summary>
	public interface IDropContainer
	{
		/// <summary>
		/// Handles the drop of the specified IDroppable item onto the container.
		/// </summary>
		/// <param name="item"></param>
		void Drop(IDroppable item);
	}
}

The DragDropManager (and and associated delegate and event args class) are defined as such:

namespace Lionsguard
{
    /// <summary>
    /// Represents a class used to manage drag and drop items.
    /// </summary>
	public class DragDropManager
	{
		private Panel Host { get; set; }
		private Point MousePosition { get; set; }
		private UIElement DragCursor { get; set; }	
		private bool HasProcessedEndDrag { get; set; }
		private IDroppable Item { get; set; }	

        /// <summary>
        /// Initializes a new instance of the DragDropManager class.
        /// </summary>
        /// <param name="host">The hosting panel where the drag cursor instance will be added.</param>
		public DragDropManager(Panel host)
		{
			this.Host = host;
			this.Host.LostFocus += new RoutedEventHandler(OnHostLostFocus);
		}

		private void OnHostLostFocus(object sender, RoutedEventArgs e)
		{
			EndDrag(false);
		}

		private void OnDragCursorLostMouseCapture(object sender, MouseEventArgs e)
		{
			EndDrag(false);
		}

		private void OnDragCursorLostFocus(object sender, RoutedEventArgs e)
		{
			EndDrag(false);
		}

		private void OnDragCursorMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
		{
			((UIElement)sender).ReleaseMouseCapture();

			this.MousePosition = e.GetPosition(null);

			this.EndDrag(true);
		}

		private void OnDragCursorMouseMove(object sender, MouseEventArgs e)
		{
			Point position = e.GetPosition(null);

			double deltaX = position.X - this.MousePosition.X;
			double deltaY = position.Y - this.MousePosition.Y;

			Point newPosition = new Point(
				((double)this.DragCursor.GetValue(Canvas.LeftProperty)) + deltaX,
				((double)this.DragCursor.GetValue(Canvas.TopProperty)) + deltaY);

			this.DragCursor.SetValue(Canvas.LeftProperty, newPosition.X);
			this.DragCursor.SetValue(Canvas.TopProperty, newPosition.Y);

			this.MousePosition = position;
		}

        /// <summary>
        /// Begins a drag operation for the specified IDroppable instance starting at the specified mousePosition.
        /// </summary>
        /// <param name="droppable">The IDroppable instance to being dragging.</param>
        /// <param name="mousePosition">The Point at which the drag operation will begin.</param>
		public void BeginDrag(IDroppable droppable, Point mousePosition)
		{
			if (droppable != null && this.Host != null)
			{
				System.Diagnostics.Debug.WriteLine("Begin Drag...");
				this.Item = droppable;

				this.DragCursor = droppable.GetDragCursor();
				this.DragCursor.LostFocus += new RoutedEventHandler(OnDragCursorLostFocus);
				this.DragCursor.LostMouseCapture += new MouseEventHandler(OnDragCursorLostMouseCapture);

				// Add the drag cursor to the host control and set the z-index very high.
				this.DragCursor.SetValue(Canvas.ZIndexProperty, 5000);
				this.Host.Children.Add(this.DragCursor);
				this.Host.Cursor = Cursors.Hand;

				this.MousePosition = mousePosition;
				this.DragCursor.SetValue(Canvas.LeftProperty, this.MousePosition.X);
				this.DragCursor.SetValue(Canvas.TopProperty, this.MousePosition.Y);
				this.DragCursor.Visibility = Visibility.Visible;

				this.HasProcessedEndDrag = false;

				this.DragCursor.CaptureMouse();

				this.DragCursor.MouseLeftButtonUp += new MouseButtonEventHandler(OnDragCursorMouseLeftButtonUp);
				this.DragCursor.MouseMove += new MouseEventHandler(OnDragCursorMouseMove);
			}
		}

		private void EndDrag(bool processDrop)
		{
			// This prevents the mouse up event or the end drag event firing more than once from causing the
			// events to bubble up and the drop be handled more than once.
			System.Diagnostics.Debug.WriteLine("End Drag...");
			if (this.HasProcessedEndDrag) return;
			this.HasProcessedEndDrag = true;

			// Reshow the cursor and hide the source image.
			this.Host.Cursor = Cursors.Arrow;

			// Hide the drag cursor.
			if (this.DragCursor != null)
			{
				this.DragCursor.Visibility = Visibility.Collapsed;

                this.DragCursor.LostFocus -= new RoutedEventHandler(OnDragCursorLostFocus);
                this.DragCursor.LostMouseCapture -= new MouseEventHandler(OnDragCursorLostMouseCapture);
				this.DragCursor.MouseLeftButtonUp -= new MouseButtonEventHandler(OnDragCursorMouseLeftButtonUp);
				this.DragCursor.MouseMove -= new MouseEventHandler(OnDragCursorMouseMove);
			}

			if (processDrop)
			{
				System.Diagnostics.Debug.WriteLine("Process Drop...");
				// Search within host for an IDropContainer instance within the area of the current mouse position.
				var container = (from v in VisualTreeHelper.FindElementsInHostCoordinates(this.MousePosition, this.Host)
								where (v as IDropContainer) != null
								select v as IDropContainer).FirstOrDefault();
				if (container != null)
				{
					System.Diagnostics.Debug.WriteLine("Drop...");
					container.Drop(this.Item);
				}
			}
		}
	}

    /// <summary>
    /// Provides a delegate for handling IDroppable.BeginDrag events.
    /// </summary>
    /// <param name="sender">The IDroppable initiating the drag operation.</param>
    /// <param name="e">Event data for the operation.</param>
	public delegate void BeginDragEventHandler(object sender, BeginDragEventArgs e);

    /// <summary>
    /// Represents event data for a BeginDrag event.
    /// </summary>
	public class BeginDragEventArgs : EventArgs
	{
        /// <summary>
        /// Gets or sets the IDroppable initiating the event.
        /// </summary>
		public IDroppable Droppable { get; set; }

        /// <summary>
        /// Gets or sets the Point at which the drag operation should begin.
        /// </summary>
		public Point MousePosition { get; set; }	
	}
}
To use the manager is your project have the controls wyou wish to drag and drop implement the IDroppable interface and the destinations for the drop operation should implement the IDropContainer. One control could implement both if you have operations where items can be dragged into other items of the same type.
Here is an example of the IDroppable and IDropContainer implementation (Both of which are in the same user control).
The GetDragCursor method creates the item so it looks like you are dragging it across the screen, you can have any kind of cursor replacement UIElement you want with this.

namespace Perenthia.Controls
{
public partial class AvatarListItem : UserControl, IDroppable, IDropContainer
{

#region IDroppable Members

public event BeginDragEventHandler BeginDrag = delegate { };

public UIElement GetDragCursor()
{
Image img = new Image();
img.Source = this.GetImageSource();
img.Width = 16;
img.Height = 16;
Border border = new Border();
border.Background = Brushes.DialogFillBrush;
border.BorderBrush = Brushes.BorderBrush;
border.Padding = new Thickness(2);
border.Width = 18;
border.Height = 18;
border.Child = img;
return border;
}

#endregion

private void NameLabel_MouseLeave(object sender, MouseEventArgs e)
{
if (_details != null)
{
_details.Hide();
}
PopupManager.Remove();
}

#region IDropContainer Members

public void Drop(IDroppable item)
{
// Do stuff when the object is dropped...
}

#endregion
}
}

And here is an example of the usage of the drag and drop manager (Contained within a silverlight Page class):

I declare an instance of the DragDropManager when my control is loaded (I use a Canvas for LayoutRoot as I find the drag operation to be smoother with a Canvas).
private void AdminScreen_Loaded(object sender, RoutedEventArgs e)
{
_dragDropManager = new DragDropManager(LayoutRoot);
}

When my IDroppable control instance is created I register a listener to the BeginDrag event:
avatarItem.BeginDrag += new BeginDragEventHandler(OnDroppableBeginDrag);

When the IDroppable raises the BeginDrag event, on a left mouse button click, this method is called which causes the DragDropManager to being the drag operation for the current IDroppable.

private void OnDroppableBeginDrag(object sender, BeginDragEventArgs e)
		{
			if (_dragDropManager != null)
			{
				_dragDropManager.BeginDrag(e.Droppable, e.MousePosition);
			}
		}

When an IDroppable is dropped onto the very same control this method is executed:

#region IDropContainer Members
		public void Drop(IDroppable item)
		{
                    // Do stuff when the object is dropped...
		}
		#endregion

This is pretty much all there is to it.

 

4/21/2009