I've been meaning to tap this out for a while.
on a recent project (T5.1), i found tapestry's XHR handling/zone
updating architecture lacking. I do hope there's a better way to achieve
what i needed to which is why i'm writing this. if there is not i think
there's a strong case to tidy the implementation up somewhat. it's
certainly not that tapestry can't provide the functionality required,
just that the API doesn't feel particularly well thought out in this one
area. i'll explain...
1. Using the zone parameter to flag an event as XHR
To specify that you want to perform an XHR request (EventLink,
ActionLink, Form etc), you need to supply the zone parameter. The
existence of this parameter is a flag that tells the component to use
XHR. In some cases this may be useful (although I'm yet to find one).
This strikes me as bad design since there is not necessarily a known
One-to-One relationship between the event and the zone(s) updated.
Since all of my XHR event handlers return a MultiZoneUpdate, I ended up
creating a dummy zone on every page and component and supplying that to
every zone parameter. The dummy zone was always hidden and never
actually updated. This hack made it easier to code and maintain my pages.
Obviously an improvement would be to add a 'xhr' parameter to the
components (EventLink, Form etc) and for zone to be an optional
parameter if xhr is true. You could even hard set xhr=true if zone !=
null for backwards compatibility.
2. MultiZoneUpdate is a chain of MultiZoneUpdates
Once you're in the java implementation of your event handler, you can
either return a Zone object, or a MultiZoneUpdate object. (if you've
supplied a non-dummy zone you can just return a "renderer" which will be
inserted into the zone). If you (like me) want to return a
MultiZoneUpdate you need to:
1. test exists = false therefore create new MultiZoneUpdate(zoneId,
zone) and store as component variable
2. test exists = false therefore add subsequent
MultiZoneUpdate(zoneId, zone)s to the initial MultiZoneUpdate
3. return the initial MultiZoneUpdate
Since sometimes due to method re-use etc a particular statement may or
may not be the one which creates the initial MultiZoneUpdate, and some
re-used methods would add updates which had already been added by
another method. I found I needed to create a utility class -
MultiZoneUpdateManager - to manage the MultiZoneUpdate chain building.
This solved two issues: testing whether the initial MultiZoneUpdate was
initialised and avoiding duplicate Zones within the chain. The process
was simplified to:
1. create new MultiZoneUpdateManager() update and store as component
variable
2. MultiZoneUpdateManager.add(zoneId, zone)
3. return MultiZoneUpdateManager.getMultiZoneUpdate()
I would suggest the refactoring of MultiZoneUpdate so that it
1. can be constructed without a zone update request
2. keys updates by zoneId (no point performing multiple updates on a
single zone)
3. behaves as if 'null' was returned (from the event handler) if it
is returned with no zone updates
I don't see any reason why this couldn't be backwards compatible.
3. XHR requests should be easily callable from javascript
I quite often needed to initialise an XHR request-response from
javascript. I ended up writing a function to facilitate this. It handles:
1. the zone wiring (note the need for a dummy zone shows up here too)
2. context parameters (unfortunately does not properly conform to
encoding rules as per server side generated params)
3. query strings (sometimes useful)
4. url based session ids (for when cookies are disabled)
note that the url is usually generated at the server side via
ComponentResources.createEventLink.
function multiZoneUpdate(url, params, zoneId)
{
if (typeof(zoneId) == "undefined")
zoneId = "dummyZone";
var zoneObject = Tapestry.findZoneManagerForZone(zoneId);
if (!zoneObject)
throw "unknown zone: " + zoneId;
if (!(params instanceof Array))
params = [params];
var qs = "";
var qsInd = url.indexOf("?");
if (qsInd != -1)
{
qs = url.substring(qsInd);
url = url.substring(0, qsInd);
}
var jsId = "";
var jsInd = url.indexOf(";");
if (jsInd != -1)
{
jsId = url.substring(jsInd);
url = url.substring(0, jsInd);
}
if (params != null)
for (var p = 0; p < params.length; p++)
url += "/" + params[p];
zoneObject.updateFromURL(url + jsId + qs);
}
It would be preferable if tapestry could create the XHR request without
a zoneObject (see point 1) and exposed a method to mimic the encoding of
context parameters.
Summary
For the most part, tapestry has been a breeze to work with, however It
would be easier to continue developing complicated ajax applications
with these few changes. I hope I can either be pointed in a better
direction, or that these ideas are considered in future releases.
Cheers, Paul.