Monday, August 30, 2010

File Promises and dojo drag and drop

When Adobe pushed AIR 2.0 out via auto update, one of the first new API features that I wanted to try to take advantage of is the File Promises API. Armed with Christian Cantrell's article, I went about integrating File Promises into my application.

Drag and drop isn't a new interaction paradigm in the application I work on. Users have always been able to drag and drop items in the application thanks to dojo's drag and drop APIs. With AIR 2.0 and File Promises though, I wanted to give the user the ability to drag files internally in the application and also out of the application and onto their desktop. To allow both internal and external drag and drop operations, I needed to override the default dojo.dnd.Avatar. Overriding the default Avatar for dojo dnd operations is really easy - the constructor for your dnd source should include the following line (basically, you need some way of telling the dnd manager to use a different function when creating the avatar):
dojo.dnd.manager().makeAvatar = dojo.hitch(this, "makeAvatar");

You'll need a makeAvatar function which returns your custom Avatar:
makeAvatar: function() {
return new my.widget.FileListAvatar(dojo.dnd.manager());
},

Once you've told the dojo dnd manager to use your custom dnd avatar, you'll need to figure out when the avatar in question is being dragged close to the edge of the application. See the _handleMouseMove function below:

dojo.declare(
"my.widget.FileListAvatar",
[dojo.dnd.Avatar],
{
_mouseOverConnects: [],

construct: function() {
// override and cancel the autoScroll function to prevent whitespace from appearing
// in the application when the user is dragging files near the border.
dojo.dnd.autoScroll = function(){};
this._mouseOverConnects.push(dojo.connect(this.manager, "onMouseMove", this, "_handleMouseMove"));
}

_handleMouseMove: function() {
var pos, viewport = dojo.window.getBox();
if(this.node) {
pos = dojo.position(this.node);
if(viewport.w <= pos.x + pos.w || pos.x <= 0
|| viewport.h <= pos.y + pos.h || pos.y + pos.h <= 0) {
// at this stage we know that the user is dragging the avatar close to the application's boundaries.
// time to make a switch from an internal dnd operation to an external dnd.
this. _handleExternalDnd();
}
}

The _handleExternalDnd is the point in the application where we start using the AIR 2.0 file promises API. In the following code, all the items in my dnd source have a store and item property which contain information on the url and file name. The url property from each dnd item is passed to a new URLFilePromise object.
_handleExternalDnd: function() {
var f, url, cb = new air.Clipboard(), promises = [], vp = dojo.window.getBox(), isDraggedOut = false;
for(var j = 0, k = this.manager.nodes.length; j < k; j++) {
f = this.manager.source.getItem(this.manager.nodes[j].id).data;
url = f.store.getValue(f.item, "Url");
if(url !== "") {
var fp = new air.URLFilePromise();
fp.request = new air.URLRequest(url);
fp.relativePath = f.store.getValue(f.item, "FileName");
promises.push(fp);
}
}
cb.setData(air.ClipboardFormats.FILE_PROMISE_LIST_FORMAT, promises);
// the next line causes the switch from internal dnd to external dnd
air.NativeDragManager.doDrag(window.htmlLoader, cb);
air.NativeApplication.nativeApplication.activeWindow.stage.addEventListener(window.runtime.flash.events.NativeDragEvent.NATIVE_DRAG_UPDATE, function(evt) {
if(!isDraggedOut && evt.localX > vp.w) {
isDraggedOut = true;
} else if (isDraggedOut && evt.localX < vp.w) {
// TODO - this is the case where the file has been dragged out of the application and the user is dragging the file back in.
// would be nice at this stage to revert to revert back to an internal dnd operation.
}
});
dojo.style(this.node, "display", "none");
},

I've left some commented code in there which might give you hints of what I'd like to do when the user drags files back into the application after initially dragging them out. I haven't figured out how to cancel a native drag and drop operation just yet - if I do, I'll be sure to update this blog post.

That's pretty much it. Once the user drags files out of the application and into their native file system browser, the urls included in the URLFilePromise object will be downloaded. Check out some of the events you can connect to if you want to give your users a progress indicator of where their files are in terms of being downloaded. One point to note when using File Promises - users are only allowed to drag files from an AIR application into the default file browser. For example, the user cannot drag files directly from an Adobe AIR application into iPhoto or Lightroom. They need to drag the files into Finder first, wait for the files to be downloaded and then drag them into the file management application of their choice.

Looking to the future, it looks like this type of functionality will start to become available natively in the browser.

Some useful links which might help with further reading:

Getting started with dojo dnd
dojocampus' dnd articles
Advanced dojo dnd tutorial
CSS Ninja article on drag and drop
CSS Ninja article how gmail's dnd works

Monday, August 23, 2010

dojo.connect Widget gotcha

I'm not too sure this small issue I ran into actually constitutes a full blog post, but here goes all the same. After reading phiggins' article on how to dispose of programatically created dijits, I started keeping track of the handles returned from dojo.connects and dojo.subscribes. I figured that these return values should be disconnected/unsubscribed just before a widget was destroyed to aid with garbage collection. Hence, a lot of my widgets started to look like this:
dojo.declare(
"my.widget.AccountCreator",
[dijit._Widget, dijit._Templated],
{
templatePath: dojo.moduleUrl("my.widget", "templates/AccountCreator.html"),
widgetsInTemplate: true,
_subscriptions: [],
_connects: [],

postCreate: function() {
this._connects.push(dojo.connect(dijit.byId("accountCreatorDialog"), "hide", this, "_handleHideDialog"));
this._subscriptions.push(dojo.subscribe("/acct/created", this, "_handleAccountCreated"));
},

destroy: function() {
// first clean up my connects and handles
dojo.forEach(this._subscriptions, dojo.unsubscribe);
dojo.forEach(this._connects, dojo.disconnect);
this._subscriptions = [];
this._connects = [];
// now go ahead and delete the rest of the widget
this.inherited(arguments);
}

Seems relatively simple, right? Being a good memory citizen and cleaning up just before the widget gets destroyed seems like a win-win situation. However, notice that my collection of dojo.connect handles is named _connects. Check out the source code for dijit._Widget (my.widget.AccountCreator inherits from dijit._Widget):
  create: function(/*Object?*/params, /*DomNode|String?*/srcNodeRef){
// summary:
// Kick off the life-cycle of a widget
// params:
// Hash of initialization parameters for widget, including
// scalar values (like title, duration etc.) and functions,
// typically callbacks like onClick.
// srcNodeRef:
// If a srcNodeRef (DOM node) is specified:
// - use srcNodeRef.innerHTML as my contents
// - if this is a behavioral widget then apply behavior
// to that srcNodeRef
// - otherwise, replace srcNodeRef with my generated DOM
// tree
// description:
// Create calls a number of widget methods (postMixInProperties, buildRendering, postCreate,
// etc.), some of which of you'll want to override. See http://docs.dojocampus.org/dijit/_Widget
// for a discussion of the widget creation lifecycle.
//
// Of course, adventurous developers could override create entirely, but this should
// only be done as a last resort.
// tags:
// private

// store pointer to original DOM tree
this.srcNodeRef = dojo.byId(srcNodeRef);

// For garbage collection. An array of handles returned by Widget.connect()
// Each handle returned from Widget.connect() is an array of handles from dojo.connect()
this._connects = [];

Having a property of _connects on a custom dijit which inherits from dijit._Widget landed me in trouble. When my.widget.AccountCreator's destroy method was called, the call to dojo.disconnect was throwing an odd looking error:
Result of expression '([dojo._listener, del, node_listener][listener])' [undefined] is not an object

As pointed out by both @neonstalwart and @cb1kenobi (thanks!), there's a simple solution to all this - just use the connect method in dijit._Widget.
this.connect(dijit.byId("accountCreatorDialog"), "hide", "_handleHideDialog");

dijit._Widget's connect method will also take care of automatically disconnect each dojo.connect handle when the widget is destroyed. As a result, you don't need to override the destroy method of your custom widget to clean up any lingering handles.

Have you ever stumbled into an issue like this - are there any other 'common' internal dojo variable names that you've unconsciously clobbered?

Wednesday, August 18, 2010

Migrating to dojo 1.5

The application I work on had been running the 1.3 version of dojo for the past 18 months or so. I procrastinated with upgrading to the 1.4 version as I was busy migrating an existing web based application to an Adobe AIR app. When dojo 1.5 rolled around I had some spare cycles, so I decided to make the jump directly from 1.3 to 1.5. As with any software upgrade there were some hiccups along the way - here's the few issues I stumbled across:

1. dijit.layout.TabContainer's default controllerWidget seems to have switched from dijit.layout.TabController to dijit.layout.ScrollingTabController. This caused some issues for me as I had declared some custom CSS stylings specifically for dijit.layout.TabController which didn't apply to dijit.layout.ScrollingTabController. Pretty simple workaround for this issue though - I just needed to specify a controllerWidget property when programatically creating new dijit.layout.TabContainer widgets.

2. dojox.layout.RotatorContainer/dojox.layout.RotatorPager have been replaced by a combination of dojox.widget.AutoRotator/dojox.widget.rotator.Controller. Given that I had hacked into a lot of 'private' APIs in the 1.3 version of dojox.layout.RotatorContainer I was a little apprehensive about making the move to dojox.widget.AutoRotator. @cb1kenobi on the dojo IRC helped out a lot with the migration and my application code is a lot cleaner looking as a result.

3. dojo.deferred got a makeover in the 1.5 version of dojo. Previously, I had code which looked like this:
dojox.rpc.transportRegistry.register(
"JSONP",
function(str){ return str === "JSONP"; },
{
fire: function(r) {
var headers = [], url, def;
url = r.target + ((r.target.indexOf("?") === -1) ? '?' : '&') + r.data;
// dair.xhr.send returns a new dojo.Deferred
def = dair.xhr.send({
url: url,
method: "POST",
headers: headers,
checkHeaders: true
});
def.addCallback(this, "parseResults");
return def;
},

parseResults: function(obj) {
obj = obj.data;
var result = dojo.fromJson(obj);
if(result && result.Error) {
var errCode = new Error(result.Error.Code);
errCode.displayMsg = result.Error.Message;
return errCode;
}
return result;
},

A short explanation of the code above:

1. Make the network call (dair.xhr.send).
2. Parse the results with a dojo.fromJson call in parseResults.
3. If while parsing the results of a network call I found an Error property on the json object, treat this response as an error.
4. Returning an Error object from the parseResults method resulted in the error callback being invoked.

However, with the 1.5 version of dojo, this doesn't fly. After pestering @novemberborn on the #dojo IRC channel, he was able to point me in the right direction - I needed an extra dojo.deferred (note the promise variable is what I invoke the callback/errback on in the example below):

dojox.rpc.transportRegistry.register(
"JSONP",
function(str){ return str === "JSONP"; },
{
fire: function(r) {
var headers = [], url, def, promise = new dojo.Deferred();
url = r.target + ((r.target.indexOf("?") === -1) ? '?' : '&amp;') + r.data;
def = dair.xhr.send({
url: url,
method: "POST",
headers: headers,
checkHeaders: true
});
def.then(dojo.hitch(this, function(obj) {
this.parseResults(obj, promise);
}));
return promise;
},

parseResults: function(obj, promise) {
obj = obj.data;
result = dojo.fromJson(obj);
if(result &amp;&amp; result.Error) {
resErr = new Error(result.Error.Code);
resErr.displayMsg = result.Error.Message;
promise.errback(resErr);
return;
}
if(promise.fired !== 1) {
promise.callback(result);
}
},


Apart from some other styling bits and bobs, those 3 issues were the only pieces that slowed me down when doing the migration. Having migrated dojo applications from 0.43 -> 0.9 -> 1.0.2 -> 1.3 -> 1.5, I have to say that this migration ranks among the easier of all migrations. This speaks volumes for the dojo dev team - keeping true to an API is definitely changeling at times, but it makes my life a hellava lot easier. I always find that migration time is always a good time to take a second look at how you're using a toolkit. For me, I started using the .set/.get APIs instead of the .attr API. I also converted by dojo.deferred addCallback/addErrback methods to the more concise dojo.then. I also migrated some custom code to use the new dojox dnd BoundingBoxController and Selector APIs (shameless plug there). When the dust settled after the migration, I figured out that I was able to get rid of ~250 lines of custom code as I had found alternatives in the dojo toolkit - I'm always glad to get rid of code. All in all, I reckon the migration took around 4 days of work - not too bad considering the application I work on is ~11000 lines of code.

dojo 1.6 seems to be coming up pretty soon - I reckon I won't leave it another 18 months for my next dojo upgrade. http://bugs.dojotoolkit.org/ticket/8578 looks particularly interesting - I've been looking for some CI implementation for dojo doh testing and this ticket looks like it could prove very useful.