Flex Multi-Drag

Have you ever been playing with the DragManager in your flex app and wondered  how to get more than one object dragging at once? DataGrid and List can do it, so why not other custom widgets? On a project I’ve been working on, we have a custom schedule component and want to drag multiple items around at once, so I did a little research and found out how it is done. The Flex SDK, which you can download for free, comes with source code! On my Mac, it was installed in the Flex Builder directory (/Applications/Adobe Flex Builder 3/sdks/3.2.0/frameworks/projects/framework/src/). Within there, I found what I was looking for in mx/controls/listClasses/ListBase.as

It turns out that DataGrid and List extend ListBase, which implements multi-drag for those components. If you look at the documentation for DragSource, it has an addHandler(..) method, that allows you to pass a collection along with the drag. When DragEvent.DRAG_DROP is being handled, you can pull that collection from the DragSource. Adding the objects from that collection is a manual process, but at least it is easy to pass all of the objects you’d like to drag along without much fuss. Here’s a bit of code that demonstrates this;

var dragSource: DragSource = new DragSource();
ds.addHandler(copySelectedItems, "items");
DragManager.doDrag(this, dragSource, event, dragImage, 0, 0, 0.5, dragMoveEnabled);

A really cool thing here is that “addHandler(..)” takes a method that provides the items. That method only gets called when the data is actually needed. So, you can defer data access until a drop is actually being performed. In the case where you have data to be added and just want to provide it up front, you can call “addData(..)” instead, which will take your array. The part of drag and drop that is a little more complicated with multi-drag is having a convincing looking drag proxy. If you don’t know, the drag proxy is what the user sees being moved around the screen during a drag. We’ve been creating bitmaps of the items we move instead of using the ghosted outline that is the default. When dragging multiple objects, we had to create a bunch of bitmaps and translation matricies that are used to create a bigger bitmap (which becomes the drag proxy).

To get the data in the DRAG_DROP handler, you can use some code like this;

if (event.dragSource.hasFormat("items"))
    var items:Array = event.dragSource.dataForFormat("items") as Array;
}

The default drag proxy is OK, but we have been creating custom bitmaps to give a nicer “drag” appearance. This got more complicated with multi-drag. The solution involves iterating through the separate drag items and creating separate bitmaps with transform matricies. Using those, and some other variables to track the bounding box for all of the items, it is possible to build a larger bitmap and paste the smaller images in (using the positional transforms). If you do this, don’t forget to apply a transform to the final drag proxy image to offset the actual click location within the larger bitmap. If you don’t your drag proxy will render with its upper left corner at the mouse, instead of where it should be.

Data Push with GDS Gravity and Tomcat

I’ve been working on a project that uses the very nice application stack of mysql-hibernate-graniteds-flex and recently decided I needed to push data from the server to the client. Since I was already using GraniteDS for AMF remoting, I thought I’d take advantage of the Gravity package which does a Comet-like data push. The GDS documentation has some of the information I needed. Since I was running on OS X, I also needed these instructions for getting APR installed with tomcat (which gravity requires for native I/O). BTW, from what I read, APR is a must for any production use of tomcat since it increases I/O performance through native calls to the OS.

After getting this all set up, I was able to get my channel configured and subscribe to it from the flex client. In my tomcat log, I could see how the comet request timeouts happened at regular intervals (as you’d expect). I wasn’t, however, getting messages sent to the client. Since that is the whole purpose of this exercise, getting this working was very important!

After digging through the gravity code, it seems that the thing that was missing is that the sub_topic header needs to be set on the message. Once I set that to be the same as the client was expecting, it worked great! Here is the code for a servlet I extend for some REST web services which need to send a message to my flex app.

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

import flex.messaging.messages.AsyncMessage;
import flex.messaging.messages.Message;
import org.granite.gravity.AbstractChannel;
import org.granite.gravity.Gravity;
import org.granite.gravity.tomcat.TomcatChannelFactory;

public class GravityServlet extends HttpServlet {
	private Gravity gravity;
	private AbstractChannel pubChannel;

	public void init(ServletConfig config) throws ServletException {
		gravity = (Gravity)config.getServletContext().getAttribute("org.granite.gravity.Gravity");
		pubChannel = new AbstractChannel(gravity) {
				@Override protected void clearQueue() { }
				@Override public void deliver(
						AbstractChannel from,
						Message message,
						String subscriptionId
					) { }
   			 };
		gravity.registerChannel(pubChannel);
	}

	protected void sendMessage(String msg) {
		AsyncMessage message = new AsyncMessage();
		message.setBody(msg);
		message.setHeader(AsyncMessage.SUBTOPIC_HEADER, "discussion");
		message.setDestination("etlprocess");
		gravity.publishMessage(pubChannel, message);
	}
}

Here is the code that registers to receive the messages;

	// this code sets up listener for server push of EDL update status
	consumer = new Consumer();
	consumer.destination = "etlprocess";
	consumer.topic = "discussion";
	consumer.subscribe();
	consumer.addEventListener(MessageEvent.MESSAGE, handleETL);

The web.xml looks like this;

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com
/dtd/web-app_2_3.dtd">
<web-app>
    <!-- read services-config.xml file at web application startup -->
    <listener>
        <listener-class>org.granite.config.GraniteConfigListener</listener-class>
    </listener>

    <!-- handle AMF requests ([de]serialization) -->
    <filter>
        <filter-name>AMFMessageFilter</filter-name>
        <filter-class>org.granite.messaging.webapp.AMFMessageFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>AMFMessageFilter</filter-name>
        <url-pattern>/graniteamf/*</url-pattern>
    </filter-mapping>

    <!-- handle AMF requests (execution) -->
    <servlet>
        <servlet-name>AMFMessageServlet</servlet-name>
        <servlet-class>org.granite.messaging.webapp.AMFMessageServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet>
        <servlet-name>GravityServlet</servlet-name>
        <servlet-class>org.granite.gravity.tomcat.GravityTomcatServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>AMFMessageServlet</servlet-name>
        <url-pattern>/graniteamf/*</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>GravityServlet</servlet-name>
        <url-pattern>/gravity/*</url-pattern>
    </servlet-mapping>
</web-app>

One more piece that I seem to have left out. In the services-config.xml which configures graniteDS, I had to add a service def that defined my destination (in this case, “etlprocess”).

        <service id="messaging-service"
            class="flex.messaging.services.MessagingService"
            messageTypes="flex.messaging.messages.AsyncMessage">
            <adapters>
                <adapter-definition
                    id="default"
                    class="org.granite.gravity.adapters.SimpleServiceAdapter"
                    default="true"/>
            </adapters>

            <destination id="etlprocess">
                <channels>
                    <channel ref="my-gravityamf"/>
                </channels>
            </destination>
        </service>