Nathan's Tech Blog

Thursday, January 05, 2006

Why Tapestry's Crazy Rewind Is a Good Thing

For the past year and a half I've been playing around with a web application framework called Tapestry. It's a component-based framework originally written by Howard Lewis Ship and now maintained by a community of developers and users. It has many similarities to the Java Server Faces framework specification.

However, it also has some important differences. One of the most complained-about and most misunderstood aspects of Tapestry is known as the "Rewind Cycle". Recently there has been talk of removing the "Rewind Cycle" altogether. This would be an unfortunate loss.

The "Rewind Cycle" is really more of a "Page Render Replay". When Tapestry processes an incoming form, it needs to "replay" the submitting page's rendering step in order to find all of the components and their appropriate bindings. This is actually very similar to the JSF "Restore View" step, which is the first step in the JSF Standard Request Processing Life Cycle (http://java.sun.com/j2ee/1.4/docs/tutorial/doc/JSFIntro10.html). JSF usually restores the view either from serialized data in a hidden form variable or from session data stored on the server.

Tapestry, on the other hand, restores the view by re-rendering part (or all) of the page. This is the "rewind cycle" (which, of course, would be better named "replay phase" or "restore phase"). This approach to replaying thepage to restore the view has both negatives and positives.

First, the negatives.

The components that make up the page often depend on data stored in the database or in a session variable. It is possible that, between the time the page was originally rendered and when the frm POST is processed, the data could have changed. In this case, replaying the page's rendering to restore the view will yield components that are different from when the page was originally rendered. This will cause a mismatch between the data being POSTed to the application and the components intended to accept that data. This leads to the infamous "Stale Session" error in Tapestry.

This can sometimes be mitigated by using a blend of Tapestry's "page replay" strategy and JSF's "serialized data in a form field" strategy. Some tapestry components (such as "For" and "If") store data in hidden form fields. Then during "rewind" (replay/restore), the component will build itself using that data, thus providing the exact same results during "rewind" as during the original render.

Second, because Tapestry applies values from the request into the model and calles listener methods DURING the "rewind cycle", model's data may change in the middle of the re-render, and this change in the data can change the remainder of the page. This leads to a "Stale Session" error, since the components in the re-render will now no longer match the components of the original render. This issue can be difficult to mitigate.

(Note that JSF always applies request values and executes events AFTER the "restore view" phase, not during. Note also that Tapestry 4 mitigates half of this issue by "deferring" the firing of most listener methods until after the form's values have all been processed. In this case, Tapestry provides more options (both immediate and delayed event processing, while JSF only provides delayed event processing.)

Well, what about the positives?

The biggest positive is a variant of one of the negatives above. It comes from the fact that Tapestry performs the "Restore View", "Apply Request Values", and "Process Events" phases all simultaneously. This, while being confusing, is very powerful.

The result is that tapestry allows dynamic and arbitrarily complex bi-directional value data binding, including easy handling of loops. Phew... that's quite a mouthful!

Bi-directional value data binding is one of Tapestry's biggest strengths. I know very few frameworks (a.k.a. none) that do it quite as well as Tapestry does.

This is especially true when loops are involved. For example, if I want to loop through a list of values and bind each value to a TextField component, I can do that. As long as I don't let the length of the list of values change between the original render and the form POST, then Tapestry will automatically handle the bi-directional binding, even if I bind to the loop variable! That's because it is REPLAYING the loop while it is processing the POSTed values and binding the values back into my model.

Below is example using NON-TAPESTRY pseudo code. Even though it is not Tapestry, you could easily create a one-to-one conversion of this into Tapestry.

If the "rewind cycle" is truly removed from future versions of Tapestry, some very creative solution will need to be used to retain this functionality. For me, this is what makes Tapestry stand out above the others in the crowd. I can't convert my app to JSF and Seam because only Tapestry supports this complexity of bi-directional binding, which is critical for the application that I'm building. This flexibility and power needs to be preserved in future versions of Tapestry. If the rewind cycle is replaced, it needs to be replaced by something better.

---------------------------------------
Model classes:

Description: This pseudocode implements an page similar to an online survey. The question definitions are stored from a database. The definitions determine what type of question is asked, such as whether it answered by a text box, a radio button group, or a set of check boxes. The application dynamically builds the form which is automatically bound to the model representing the list of answers.

I'm using pseudocode instead of real code because I'm lazy and because my psuedocode shorter than real code. It should be easy to understand.
---------------------------------------

// note: this is pseudo code!
class Question
{
String idCode
String text
AllowedValues allowedValues
String defaultStringValue
List<String> defaultStringListValue;
}
abstract class AllowedValues
{
String type;
}
class AllowedValuesSingleSelect extends AllowedValues
{
List<Option> options;
}
class AllowedValuesMultiSelect extends AllowedValues
{
List<Option> options;
}
class Option
{
String valueCode;
String text;
}
class AllowedValuesString extends AllowedValues
{
int maxLength;
}
class AllowedValuesInteger extends AllowedValues
{
int min;
int max;
}
class Answer
{
Question question
String stringValue;
List<String> stringListValue;
}
class AnswerList extends Map<String,Answer>
{
AnswerList(List<Questions> qs)
{
// for each question, create an answer object with default value
// and a reference to the question
}
}


---------------------------------------
The view:
---------------------------------------

<html>
<body>

<!-- note: this is pseudo code, not tapestry code and not HTML and not JSF -->
<formComponent>

<foreach question in listOfQuestions>

$question.text

<if question.allowedValues.type=="SINGLE_SELECT">
<radiogroupComponent bindVariable=answers[question.idCode].stringValue>
<foreach option in question.allowedValues.options>
<radiobuttonComponent value=option.valueCode> $option.text<br/>
</foreach>
</radiogroupComponent>
</if>

<if question.allowedValues.type=="MULTI_SELECT">
<multiselectComponent bindVariable=answers[question.idCode].stringListValue
valueOptions=question.allowedValues.options/>
</if>

<if question.allowedValues.type=="STRING">
<textBoxComponent bindVariable=answers[question.idCode].stringValue
maxLength=question.allowedValues.maxLength />

</if>

<if question.allowedValues.type=="INTEGER">
<textBoxComponent bindVariable=answers[question.idCode].stringValue
validation=integerValidator(min=question.allowedValues.min,max=question.allowedValues.max) />

</if>

</foreach>

<submitButton listener="submitForm"/>

</formComponent>
<!-- note: this was pseudo code, not tapestry code and not HTML and not JSF -->

</body>
</html>


---------------------------------------
The controller:
---------------------------------------

// note: this is pseudo code!
class PageWithQuestions extends BasePage
{
DatabaseObject database; // injected from the container

List<Question> listOfQuestions; // should start as null each time page is pulled from pool to be processed
AnswerList answers; // should start as null each time page is pulled from pool to be processed

void preparePageForRenderOrRewind()
{
if (listOfQuestions==null) listOfQuestions = database.loadQuestions();
if (answers==null) answers = new AnswerList(questions);
}

public String submitForm()
{
database.saveAnswers(answers);
return "nextPage";
}
}

2 Comments:

  • Bi-directional value data binding is one of Tapestry's biggest strengths. I know very few frameworks (a.k.a. none) that do it quite as well as Tapestry does.

    Apple's WebObjects does this (and has for about a decade) and appears to do it better than Tapestry (no Stale Session errors).

    It handles loops fine, too, and the iterated object is assigned/active as soon as you are processing in the action (listener) method. There is never a need to encode database primary keys or array indexes, either, to identify which item was selected in a table/loop (using a normal hyperlink or form submission button). Just reference/message the variable the loop used to iterate over all the values. It also doesn't need to serialize objects into hidden fields.

    Of course, WebObjects isn't open source, either, although Apple does give it away for free on OS X and it was the initial inspiration source for Tapestry.

    By Anonymous Anonymous, at 1/12/2006 8:57 AM  

  • Hi Nathan,

    your articles was very helpful on tapestry rewind functionality. I create a component using pageBeginRender method but when another session access the page it has stale values from my session. Can you please give a simple examle of how exactly tapestry makes use of this functionality? Thanks alot.

    By Anonymous Anonymous, at 3/05/2006 9:52 PM  

Post a Comment

<< Home