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:
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:
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():
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:
- 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:
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:
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():
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:
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:
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:
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:
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:
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:
It’s that simple! You can find the source of the DragManager and the application above here.