Tapestry Training -- From The Source

Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!

Wednesday, April 13, 2005

Improved Listener Methods in Picasso

I'm always on the look out for ways to further simplify Tapestry development; that's one of my core principles. Simplicity. I personally love complexity ... on the inside, doing the magic that makes the end-developer's job that much easier.

One thing that's bugged people for a while concerns listener methods. These are public methods on your classes that can be invoked by forms or by the DirectLink component.

A common thing to do with DirectLink is to pass additional information in the URL. This information takes the form of listener parameters (which were confusingly called "service parameters" in 3.0 and earlier). Anyway, you can ask the request cycle for the parameters, and then extract and cast them to the right types. But that's not simple enough.

For example, suppose that the link encoded a String objectId and an integer index. The component in the template names the listener method, and the two parameters are passed into the DirectLink as an OGNL list expression:

<a jwcid="@DirectLink" listener="doClick" parameters="{ objectId, index }"> . . . </a>
This shows another recent recent improvement: the "listener:" prefix is assumed for the listener parameter, and "ognl:" is assumed for the parameters parameter. If that's not appropriate, you can still provide an explicit prefix. Kudos to Erik Hatcher for that idea.

In the Java class, the listener method might look like:

public void doClick(String objectId, int index)
  . . .

That's the innovation and simplification; the listener parameters are automatically converted into method parameters, saving you the trouble you'd have to do in release 3.0:

public void doClick(IRequestCycle cycle)
  Object[] parameters = cycle.getListenerParameters();
  String objectId = (String)parameters[0];
  int index = ((Integer)parameters[0]).intValue());
  . . .

The change is backwards compatible; there's a little search for the best matching method implementation, and the last thing checked is the Tapestry 3.0 style listener method (with the single IRequestCycle parameter).

I honestly can't remember where the idea for this came from; it seems so obvious in retrospect that you'd think I came up with up, but I think it was Erik Hatcher again. Oh, and the test suite is up to 960 tests, 277 seconds, and 82.4% code coverage.


Matt Raible said...

This is a very nice improvement - well done!

fenrick said...

I am assuming that you can still get to the request cycle through the getRequestCycle() or getPage().getRequestCycle() facilities that exist within pages and components?

If so this is a massive improvement in functionality - congrats to erik and yourself for both suggestion and implementation.

Chris Nelson said...

beautiful, Howard. I love things that make code prettier, which that definitely does. One question: I assume that the listener method could be on a nested object, ie listener="model.doSomething". I'm obviously thinking of Trail here... :)

Howard said...

This change is fully backwards compatible to 3.0.

To invoke listener methods on non-components ... look at the implemention of getListeners() on AbstractComponent and on AbstractEngine. The implementation is basically ListenerMapSource.getListenerMapForObject(this).

If you have model objects, they'll need to to implement something similar.

For Trails, a possiblity would be a new binding prefix, say "model-listener:". The binding itself could take care of obtaining the listener map and delegating to it.