Monday, October 16, 2006

KISS principle applied to Tapestry pageflow

DISCLAIMER: The code described in this blog entry was actually done by my colleague Jacob von Eyben - not me. He is the inventor, designer and implementor! I just think you all should know about it too :-)

At my current work we are doing an agile project management tool for the web and I'm so lucky to be a developer on that. The technologies in that application are hibernate for the data access, spring for the model and businesslogic layer and tapestry for view layer. We are still stuck in tapestry 3 so the code examples will be from that version.

One of the things we've kind of missed when working with tapestry is a pageflow control mechanism. Something that could guide where to go next from an action. To my knowledge, there is no such thing to tapestry 3? What we did lately was invent a very simple design to make tapestry pages publish how to navigate to them. We like to think of the solution as "Tapestry pageflow following the KISS principle".

The design consist of 3 simple interfaces:
  • ForwardCallbackHandler - implementations are capable of navigating to a specific tapestry page
  • ForwardNavigator - pages implement this if they want to have an instance of a ForwardCallbackHandler get/set on it (basically, if it wants to be told how to navigate onwards after pages action have been taken)
  • ForwardCallbackProvider - pages implement this to construct ForwardCallbackHandler instances that can navigate back to that page (basically, if it wants to let others obtain a ForwardCallbackHandler instance that can navigate back to itself)
These interfaces are used in combination with a static method navigateTo(..., ForwardCallbackHandler handler) on each page that want to make public how others can navigate to it. The "..." in the method signature covers the state that the page needs to be able to navigate to itself. It could for instance be a UserStory instance, if the navigateTo method is on a ViewUserStoryPage page.

That's a lot of text, so let's see an actual example to make it more obvious what is going on. This example is taken from Plan B, and shows:
  • One page in the application is ViewIterationPage which shows iteration information like user stories assigned to iteration, time available in iteration, ...
  • Another page in the application is EditIterationPage which is a form where iteration related information can be edited
  • When user clicks "edit" action in ViewIterationPage, the code navigates to EditIterationPage and in the process gives to EditIterationPage an instance of a ForwardCallbackHandler - a handler that can navigate back to ViewIterationPage when edit action is done
  • When user clicks save in EditIterationPage, the callback handler stored on the page is activated to perform the navigation back to ViewIterationPage
Here's the code for ViewIterationPage:

public abstract class ViewIterationPage extends ... implements ForwardCallbackProvider {
class ViewIterationCallbackHandler implements ForwardCallbackHandler {
private Iteration iteration;
public ViewIterationCallbackHandler(Iteration iteration) {
this.iteration = iteration;
}
public void forward(IRequestCycle cycle) throws TapestryRedirectException {
ViewIterationPage.navigateTo(cycle, iteration);
}
}

public ForwardCallbackHandler getForwardCallbackHandler() {
return new ViewIterationCallbackHandler(getIteration());
}

public void editIteration(IRequestCycle cycle) {
EditIterationPage.navigateTo(cycle, getIteration(), getForwardCallbackHandler());
}
}

First thing to notice is that ViewIterationPage is a provider of ForwardCallbackHandler's (as a consequence of implementing ForwardCallbackProvider).

Second thing to notice is the inner class ViewIterationCallbackHandler that is responsible of navigating from somewhere back to ViewIterationPage. To be able to navigate to ViewIterationPage, it needs to be able to fullfil the contract of the ViewIterationPage.navigateTo() method, which expects a reference to the iteration that it is going to show. No problem, as the ViewIterationCallbackHandler is a class in its own, being able to hold a Iteration instance for just that and express the need for such state through its constructor.

Last thing to notice in the aboce code is the implementation of the editIteration action. This is the code executed when "edit" link is clicked on view iteration page. It simply navigates to EditIterationPage by calling its navigateTo method which clearly defines that it needs an iteration to edit and a callback handler to be able to know where to navigate to, when edit is done.

Left is there only to show the important code in EditIterationPage:

public abstract class EditIterationPage extends ... implements ForwardNavigator {
public static void navigateTo(IRequestCycle cycle, Iteration iterationToEdit, ForwardCallbackHandler forwarder) {
EditIterationPage page = (EditIterationPage) cycle.getPage(PageNames.PAGE_EDIT_ITERATION);
page.setIteration(iterationToEdit);
page.setForwardCallbackHandler(forwarder);
throw new TapestryRedirectException(cycle, page);
}

public void editIteration(IRequestCycle cycle) {
// .... action code here to validate and save edited information
getForwardCallbackHandler().forward(cycle);
}
}

First thing is EditIterationPage implementing ForwardNavigator which includes the methods getForwardCallbackHandler() and setForwardCallbackHandler(). A ForwardCallbackHandler is serializable, so we can simply define a property named forwardCallbackHandler in the pages .page file and even make it persistent.

In the navigateTo method the page is responsible for getting an instance of itself from the tapestry page pool and setting the state needed for render. Notice that it is setting the ForwardCallbackHandler as state.

Finally, in the editIteration action method the last thing performed is navigating to ... somewhere. EditIterationPage does not know which page it is navigating to or which state the navigated to page expects to be able to render. It simply obtains its reference to a ForwardCallbackHandler (which it can because it itself is a ForwardNavigator) and calls forward() method, giving the cycle on.

Wouv, in conclusion, ... maybe this isn't so KISS after all :-) But then again, it is. A page that navigates away to somewhere else does not have to know where to. It also does not have to remember any state to set on the page forwarded to. There's no extra pageflow configuration file setup with small state machines and the like. Simply code. There are many shortcomings of this design too, I'm sure. But I expect it to be more than enough for many Tapestry applications out there.

Good work Jacob!