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?

5 comments:

  1. just use this.connect and this.subscribe in the widget and it will add your connects to _connect and subscriptions to _subscribes AND it will automatically disconnect/unsubscribe them when the widget is destroyed. it also conveniently automatically sets the context of the callback to be the widget so that you can reduce something like this:

    this._handles.push(dojo.connect(dijit.byId("accountCreatorDialog"), "hide", this, "_handleHideDialog"));
    // don't forget to do the disconnect!

    into something like this:

    this.connect(dijit.byId("accountCreatorDialog"), "hide", "_handleHideDialog"));

    and it will do the disconnect for you automatically when the widget is destroyed.

    as for clashes with internal variable names - i've been bitten a couple of times by 'selected' (added by a stack container to the currently selected child widget, and since it's added by an external widget it's not even declared in the widget that breaks and takes a while to figure out how it got there) and 'layout' (inherited from dijit.layout._LayoutWidget).

    ReplyDelete
  2. @neonstalwart - didn't even think about reusing the this.connect method in dijit._Widget. That'll work nicely - thanks for the heads up.

    ReplyDelete
  3. @neonstalwart is correct. The only time I call dojo.connect() inside a widget is when I want to connect temporarily, then disconnect.

    For example, I may have a custom widget that displays a list of items and I want to listen for when an item is clicked. If I ever needed to redraw the list, I can quickly disconnect each item before destroying the DOM nodes, then recreate & reconnect the onclick event. I could also create a custom widget for list items, but I'm generally lazy. :)

    ReplyDelete
  4. @neonstalwart, @CB1 - edited the blog post to include your suggestions. Cheers!

    ReplyDelete
  5. One more thing, there's a problem with declarations in the form:

    dojo.declare("my.ClassDef", [], {
    myVar: [] // <--- Problem!
    });

    In that case, all your objects will refer to the same array. You need to initialize 'myVar' in your constructor method to avoid that.

    From the example in this post, _connects have that problem.

    ReplyDelete