In RIA’s it often happens that you need some kind of drag and drop support. Flex has provided in this support, with it’s DragManager class. This is a class with static methods like StartDrag, you only have to supply a UIComponent, and the DragManager creates a nice transparent image, which follows the mouse and gives you visual feedback about whether the components underneath your mouse will accept the drop. Silverlight was lacking drag and drop support, until now that is!
Creating The DragManager class
The challenge of building a DragManager class, is to keep a generated proxy image, in sync with the mouse cursor. This is harder in Silverlight than in Flex, because the root visual of Silverlight, can be any Panel. In Flex you can just paint on the highest container in absolute coordinates, this isn’t the way it works in Silverlight. If the top Panel of Silverlight is a StackPanel for example, there isn’t any support for absolute coordinates, so keeping a generated proxy image in sync with the mouse cursor seems impossible. Luckily, the TranslateTransform class comes to the rescue. If we can find out the starting position of the drag image (after adding it to the root visual), we can then calculate the difference between that position and the mouse cursor. With that difference we could translate transform the generated drag image to the mouse cursor position. This way, we can make our DragManager, work with every Panel and not only the Canvas!
We will go through this in a couple of steps:
- First, creating a drag image, this a snapshot / photo of the control that initiates the drag.
- Second, finding the root visual of the application.
- Third, adding the drag image to the root visual, and getting it’s absolute coordinates.
- Fourth, on movement, keeping the drag image in sync with the mouse and giving visual feedback whether being above a drop target or not.
- Fifth, handling the moment when the user releases the mouse, acting appropriately when over a drop target, and even showing a nice animation :).
Creating a Drag Image and finding the root visual
Users of the DragManager class can start a drag by calling the StartDrag method, after registering a droptarget. StartDrag usually gets called in response to a MouseLeftButtonDown event. Take a look at the following code, which comes from my StartDrag method:
1: /// <summary>
2: /// Starts the drag, displaying an image of the drag initiator, which will follow the mouse until
3: /// the mouse button is released. A drop target must be registered, before calling this method.
4: /// </summary>
5: /// <param name="dragInitiator">Usually the control which receives the MouseLeftbuttonDown
6: /// event which starts the dragging.</param>
7: /// <param name="args">The MouseEventArgs associated with the draginitiator's MouseLeftButtonDown event.</param>
8: /// <param name="dragData">Data that the drop target is interested in.</param>
9: /// <param name="proxyAlpha">Transparency of the shown drag proxy image.</param>
10: public static void StartDrag(FrameworkElement dragInitiator,MouseEventArgs eventArgs, T dragData, double proxyAlpha)
11: {
12: //Drag animation still playing, so return.
13: if (IsDragging)
14: {
15: return;
16: }
17: //Can't drag without a drop target!
18: if (_dropTarget == null || _dropHandler == null)
19: {
20: throw new InvalidOperationException("Can't start a drag without a droptarget. Register a droptarget first!");
21: }
22:
23: //Root needed, dragging must be on top level container
24: _root = (Panel)VisualTreeHelper.GetChild(Application.Current.RootVisual, 0);
25: _dragInitiator = dragInitiator;
26:
27: //Creating the proxy image, which is a snapshot of the draginitiator
28: Image proxyImage = new Image();
29: proxyImage.Source = new WriteableBitmap(dragInitiator,dragInitiator.RenderTransform);
30: proxyImage.Width = dragInitiator.ActualWidth;
31: proxyImage.Height = dragInitiator.ActualHeight;
32: _dragProxy = new DragProxy(proxyImage);
33: _dragData = dragData;
34: _proxyAlpha = proxyAlpha;
35: _dragProxy.Background = new SolidColorBrush(Colors.Blue);
36: _startingMouseOffSet = eventArgs.GetPosition(_dragInitiator);
37:
38: //Transforms, needed for drop animation and keeping the dragproxy in sync with the mouse
39: _currentTranslateTransform = new TranslateTransform();
40: _currentScaleTransform = new ScaleTransform();
41: proxyImage.Opacity = _proxyAlpha;
42: TransformGroup tr = new TransformGroup();
43: tr.Children.Add(_currentScaleTransform);
44: tr.Children.Add(_currentTranslateTransform);
45: _dragProxy.RenderTransform = tr;
46:
47: //The proxy gets translated, so ideal for Bitmap caching......
48: _dragProxy.CacheMode = new BitmapCache();
49:
50: //Note that we're not showing the dragproxy right away, but on the first mouse movement.
51: //This enables us to still receive ordinary mousebuttonup events on the draginitiator
52: _root.MouseMove += new MouseEventHandler(root_FirstDrag);
53: _root.MouseLeftButtonUp += new MouseButtonEventHandler(root_MouseLeftButtonUpWithOutDragging);
54:
55: }
The code is pretty well documented so I will only cover the difficult parts:
- Line 24, this line gets the root panel to which our drag image needs to be added. Application.Current.RootVisual refers to the main user control, so we need to get it’s first child through the VisualTreeHelper class.
- Line 28 – 31, these lines create the actual drag image, with the help of the new Silverlight 3 WritableBitmap class. By passing a control to the Writable Bitmap’s constructor, you can make a snapshot of any control.
- Line 32, DragProxy is a simple UserControl, which adds visual feedback when a user hovers over a droptarget.
- Line 39 – 40, creating transforms. The ScaleTransform is needed for an animation when a drop succeeds, the TranslateTransform is the important one, this one is used to keep the drag image in sync with the mouse cursor.
- Line 48, this enables GPU acceleration on the drag image, which only gets translated, so it’s a perfect candidate. Note that EnableGPUAcceleration must be set to true on the Silverlight object in the host page.
Note that we have not added the drag image to the root visual yet, if the user releases the mouse now, without moving, the drag initiator still receives the MouseLeftButtonUp event, if we added the drag image on the first click, the drag image will receive the MouseLeftButtonUp event instead of the drag initiator.
Adding the drag image to the root visual
We will add the drag image to the root visual, when the user moves the mouse for the first time, while still pressing the left mouse button. Take a look at the following code:
1: /// <summary>
2: /// This eventHandler is called when a user drags for the first time, and the proxy must become visible.
3: /// </summary>
4: private static void root_FirstDrag(object sender, MouseEventArgs e)
5: {
6: // Adding proxy first, so the starting position of the proxy that the root has given it,
7: // can be determined.
8: _root.Children.Add(_dragProxy);
9:
10: //Forcing of layout updating needed, so the starting position can be determined.
11: _root.UpdateLayout();
12: _dragProxy.CaptureMouse();
13:
14: //General transforms needed to get the positions of the dragproxy and the draginitiator
15: //in the root panel's coordinate space
16: _transform = _dragProxy.TransformToVisual(_root);
17: GeneralTransform initiatorTransform = _dragInitiator.TransformToVisual(_root);
18:
19: Point initiatorLocation = initiatorTransform.Transform(new Point(0, 0));
20: Point positionInRoot = GetProxyLocation();
21: //Saving location the proxy needs to return to, needed for animation when drag ended without dropping.
22: _returnLocation = new Point(initiatorLocation.X - positionInRoot.X, initiatorLocation.Y - positionInRoot.Y);
23:
24: //Determining the starting position to which the dragproxy needs to be translated.
25: Point mouse = e.GetPosition(_root);
26: double xDifference = mouse.X - positionInRoot.X - _startingMouseOffSet.X;
27: double yDifference = mouse.Y - positionInRoot.Y - _startingMouseOffSet.Y;
28: _startingPositionInRoot = positionInRoot;
29:
30: _currentTranslateTransform.X = xDifference;
31: _currentTranslateTransform.Y = yDifference;
32:
33: ////Adding new eventhandlers, to handle the drag and removing the old ones.
34: _dragProxy.MouseMove += dragProxy_AfterFirstDrag;
35: _dragProxy.MouseLeftButtonUp += dragProxy_MouseLeftButtonUpAfterDragging;
36: _root.MouseMove -= root_FirstDrag;
37: _root.MouseLeftButtonUp -= root_MouseLeftButtonUpWithOutDragging;
38: }
And again I will cover only the difficult parts:
- Line 11, it seems that there must be call to UpdateLayout(), forcing the root to give the just added drag proxy a position.
- Line 17 and 20, gets the absolute position the drag proxy has just gotten from the root container. Take a look at the getProxyLocationMethod():
1: /// <summary>
2: /// Gets the absolute location of the proxy.
3: /// </summary>
4: /// <returns> a point with the location of the proxy, relative to the rootvisual</returns>
5: private static Point GetProxyLocation()
6: {
7: return _transform.Transform(new Point(0, 0));
8: }
The _transform variable contains a GeneralTransform object obtained in line 17 of the first code snippet. You can use TransformToVisual() on any component to get a GeneralTransform object, which can translate coordinates relative to the receiver of the TransformToVisual() call, to coordinates relative to any other component by calling the Transform() method on the GeneralTransform object. By specifying 0,0 as the arguments to the Transform method, which is the upper left corner of the receiver of the TransformToVisual() call, you can get the absolute coordinates of a component in any Panel! This includes the Grid, StackPanel and WrapPanel. Thus, a useful extension method would be:
1: /// <summary>
2: /// Gets the absolute location of this UIElement, relative to another UIElement.
3: /// </summary>
4: /// <returns> a point with the location of the upper left corner ofthis UIElement, relative to the specified UIElement</returns>
5: public static Point GetAbsoluteLocation(this UIElement self, UIElement relativeTo)
6: {
7: return self.TransformToVisual(relativeTo).Transform(new Point(0,0));
8: }
- Line 25-31, these lines calculate the difference between the position the drag proxy has gotten from the root, and the position where the user has clicked. We need to know the difference, in order to use the TranslateTransform to translate the drag proxy to the correct position.
Keeping the drag image in sync with the mouse and giving visual feedback
Before a user of the DragManager class can call StartDrag(), a drop target and a handler must be registered first. The drop target is an UIElement, over which a user can release the left mouse button to perform a drop action and complete the drag and drop operation. The handler is an Action, which gets called when the drag and drop operation completes. First, take a look at the DragManager Class definition:
1: /// <summary>
2: /// Performs dragging operations and gives visual feedback.
3: /// </summary>
4: /// <author>Alex van Beek</author>
5: /// <typeparam name="T">Type of the drag data this dragmanager holds and the drop target must accept.</typeparam>
6: public static class DragManager<T>
You should notice that the DragManager class is a static generic class. This means, that every time a user calls a method, the user must specify the type of data associated with a drag. For example, when dragging a Product from a list to the shopping cart, the call will look like:
1: DragManager<Product>.StartDrag(sender, mouseEventArgs, selectedProduct, 0.8);
above call will usually be in an eventhandler for the MouseLeftButtonDown event. The first argument is the drag initiator, usually the “sender” argument of the event handler, in this case, the DataTemplate of the product listbox. The second is usually the “args” argument of the event handler. Third is the drag data, in this case the selected domain Product object. The last is the transparency of the shown drag proxy image.
This ensures type safety, particularly when registering a droptarget, which must occur before calling StartDrag():
1: DragManager<Product>.RegisterDropTarget(productCart, Drop);
where the first argument is the visual representation of the drop target (for a example, a shopping cart image), and the second argument in this case is an Action<Product>. This means that when a drag and drop operation completes, the specified action gets called, and receives the Product that was dragged. You can use this action to add the Product domain object that was dragged, to the shopping cart. The DragManager gives visual feedback while dragging. It shows a optionally transparent image of the drag initiator (the data template of the productListBox in this case) with a red cross drawn on top of the proxy image. This red cross changes to a green circle when dragging over the registered droptarget. In order to do this, while the user is dragging, the DragManager must do a hit test to determine whether the current mouse position is over the registered droptarget. The following code snippet is from the MouseMove event handler:
1: /// <summary>
2: /// Constantly gets called while the user is dragging the drag proxy. Tests wether the proxy is over the
3: /// droptarget and gives visual feedback.
4: /// </summary>
5: private static void dragProxy_AfterFirstDrag(object sender, MouseEventArgs e)
6: {
7: Point currentMousePosition = e.GetPosition(_root);
8: _currentTranslateTransform.X = currentMousePosition.X - _startingPositionInRoot.X - _startingMouseOffSet.X;
9: _currentTranslateTransform.Y = currentMousePosition.Y - _startingPositionInRoot.Y - _startingMouseOffSet.Y;
10:
11: if (IsOverDropTarget(currentMousePosition))
12: {
13: _dragProxy.Accept();
14: }
15: else
16: {
17: _dragProxy.Deny();
18: }
19:
20: }
The handler calculates the difference between the current mouse position and the drag proxy’s starting position so it can translate the proxy to the correct location. It also uses a helper method to test whether the mouse is over the drop target. If the mouse is over the drop target, the DragManager calls Accept() on the drag proxy, which shows a green circle instead of a red cross. The interesting bit is the IsOverDropTarget() method:
1: /// <summary>
2: /// Helper method to determine wether a user is currently dragging above the drop target.
3: /// </summary>
4: /// <param name="intersectingPoint">The current point, which must be tested for intersecting with the droptarget.</param>
5: /// <returns>true when over droptarget, false otherwise.</returns>
6: private static bool IsOverDropTarget(Point intersectingPoint)
7: {
8: IEnumerable<UIElement> collidingElements = VisualTreeHelper.FindElementsInHostCoordinates(intersectingPoint, _dropTarget);
9: return collidingElements.Any();
10: }
In Silverlight 3, the VisualTreeHelper class has a FindElementsInHostCoordinates() method, which supplied with a Point and an UIElement, finds any visuals intersecting with the supplied point that are below the supplied UIElement in the visual tree. If we supply our droptarget, it doesn’t walk the whole visual tree, but only our droptarget (and every thing below it in the visual tree). If Any() returns true, the supplied Point is intersecting with our drop target and the user can safely release the mouse button.
Handling the MouseLeftButtonUp event
When the user releases the left mouse button, we first need to determine whether the mouse is over the registered drop target. If not, the drag proxy will be animated back to it’s starting location and will then be removed from the visual tree. If so, a nice animation will be played and the registered drop handler will be called, supplying the drag data. Determining what to do, is shown below:
1: /// <summary>
2: /// Gets called when user releases left mouse button while dragging.
3: /// </summary>
4: private static void dragProxy_MouseLeftButtonUpAfterDragging(object sender, MouseButtonEventArgs e)
5: {
6: if (IsOverDropTarget(e.GetPosition(_root)))
7: {
8: //If released above the drop target, remove listeners and start the drop animation.
9: _dragProxy.ReleaseMouseCapture();
10: _dragProxy.MouseMove -= dragProxy_AfterFirstDrag;
11: DoDrop(e.GetPosition(_dragProxy));
12: }else
13: {
14: //If not released above the drop target, end the drag and animate the proxy, returning to it's
15: //original position.
16: EndDrag();
17: }
18:
19: }
Next up, the DoDrop() method, which gets called when a successful drop occurs and needs the point of the mouse cursor relative to the drag proxy, in order to correctly animate it:
1: /// <summary>
2: /// Performs the actual drop and plays the dropping animation. After the animation,
3: /// calls the Drop method on the drop target, passing the drop data to the drop target.
4: /// </summary>
5: private static void DoDrop(Point scalePoint)
6: {
7: Storyboard sb = new Storyboard();
8: DoubleAnimation an = new DoubleAnimation();
9: an.Duration = new Duration(TimeSpan.FromSeconds(1));
10: Storyboard.SetTarget(an, _currentScaleTransform);
11: Storyboard.SetTargetProperty(an, new PropertyPath(ScaleTransform.ScaleXProperty));
12: an.To = 0;
13: sb.Children.Add(an);
14: an = new DoubleAnimation();
15: an.Duration = new Duration(TimeSpan.FromSeconds(1));
16: Storyboard.SetTarget(an, _currentScaleTransform);
17: Storyboard.SetTargetProperty(an, new PropertyPath(ScaleTransform.ScaleYProperty));
18: _currentScaleTransform.CenterX = scalePoint.X;
19: _currentScaleTransform.CenterY = scalePoint.Y;
20: an.To = 0;
21: sb.Children.Add(an);
22: sb.Completed += (o, e) =>
23: {
24: _dropHandler(_dragData);
25: OnDropped(new DragEventArgs<T>(_dragData));
26: CleanUp();
27: };
28: sb.Begin();
29:
30: }
On line 22, attaching an eventhandler which gets called when the animation competes, calling the registered drop handler, firing an additional event and cleaning up.
And finally, the EndDrag() method, which gets called when the user releases the mouse button while not above the registered droptarget. This method animates the proxy back to it’s starting position:
1: /// <summary>
2: /// Ends the drag, animating the proxy back to it's starting position.
3: /// </summary>
4: private static void EndDrag()
5: {
6: Storyboard sb = new Storyboard();
7: DoubleAnimation an = new DoubleAnimation();
8: an.Duration = new Duration(TimeSpan.FromSeconds(1));
9: Storyboard.SetTarget(an, _currentTranslateTransform);
10: Storyboard.SetTargetProperty(an, new PropertyPath(TranslateTransform.XProperty));
11: an.To = _returnLocation.X;
12: sb.Children.Add(an);
13: an = new DoubleAnimation();
14: an.Duration = new Duration(TimeSpan.FromSeconds(1));
15: Storyboard.SetTarget(an, _currentTranslateTransform);
16: Storyboard.SetTargetProperty(an, new PropertyPath(TranslateTransform.YProperty));
17: an.To = _returnLocation.Y;
18: sb.Children.Add(an);
19: sb.Completed += (o, e) =>
20: {
21: OnEnded(new DragEventArgs<T>(_dragData));
22: CleanUp();
23: };
24: sb.Begin();
25: }
Using the DragManager class
I’m concluding this blog post with an example, showing you how simple it is to use my DragManager class. Below is a screenshot of a very simple demo application:
On the left is the listbox “first”, on the right is the listbox “second”. You can drag items from the left listbox to the right listbox and they will be added to the right listbox, showing a nice drag image while dragging and visual feedback. When playing with this application, you will notice that the visual feedback is quite small, this is because the dragged items are quite small. The visual feedback (red cross or green circle) is always relative to the size of the drag initiator. Below is the source code for the application shown above:
1: public partial class MainPage : UserControl
2: {
3: public MainPage()
4: {
5: InitializeComponent();
6: first.DataContext = new ObservableCollection<string>() {"Alex", "Lenneke", "Piet", "Jan"};
7: second.DataContext = new ObservableCollection<string>();
8: DragManager<string>.RegisterDropTarget(second, Drop);
9: }
10:
11: public void Drop(string dropData)
12: {
13: ((ObservableCollection<string>)second.DataContext).Add(dropData);
14: }
15:
16: private void StartDrag(object sender, MouseButtonEventArgs e)
17: {
18: TextBlock tb = (TextBlock) sender;
19: DragManager<string>.StartDrag(tb, e, tb.Text, 1);
20: }
21:
22:
23: }
It’s that simple! You can find the source of the DragManager and the application above here.
2 comments
Nice solution, but the animation is horrible.
You should probably decouple the animation from the library.
Fallon Massey
Well, thats a great idea, or to let the api user supply a storyboard which gets triggered when the user releases the mouse button….
Alex van Beek