Dynamic Page State

The properties of a page and components on the page can change during the rendering process. These are changes to the page's dynamic state.

The majority of components in an application use their bindings to pull data from the page (or from business objects reachable from the page).

A small number of components, notably the Foreach component, work the other way; pushing data back to the page (or some other component).

The Foreach component is used to loop over a set of items. It has one parameter from which it reads the list of items. A second parameter is used to write each item back to a property of its container.

For example, in our shopping cart example, we may use a Foreach to run through the list of line items in the shopping cart. Each line item identifies the product, cost and quantity.

Example 4.1. HTML template for Shopping Cart

<h1>Context of shopping cart for
<span jwcid="insertUserName">John Doe</span></h1>
<table>
  <tr>
    <th>Product</th> <th>Qty</th> <th>Price</th>
  </tr>
  <span jwcid="eachItem">
  <tr>
    <td><span jwcid="insertProductName">Product Name</span></td>
    <td><span jwcid="insertQuantity">5</span></td>
    <td><span jwcid="insertPrice">$1.50</span></td>
    <td><a jwcid="remove">remove</a></td>
  </tr>
  </span>
</table>

This example shows a reasonable template, including sample static values used when previewing the HTML layout (they are removed by Tapestry at runtime). Some areas have been glossed over, such as allowing quantities to be changed.

Component eachItem is our Foreach. It will render its body (all the text and components it wraps) several times, depending on the number of line items in the cart. On each pass it:

This continues until there are no more values in its source. Lets say this is a page that has a lineItem property that is being updated by the eachItem component. The insertProductName, insertQuantity and insertPrice components use dynamic bindings such as lineItem.productName, lineItem.quantity and lineItem.price.

Part of the page's specification would configure these embedded components.

Example 4.2. Shopping Cart Specification (excerpt)

<component id="eachItem" type="Foreach">
  <binding name="source" expression="items"/>
  <binding name="value" expression="lineItem"/>
</component>

<component id="insertProductName type="Insert">
  <binding name="value" expression="lineItem.productName"/>
</component>

<component id="insertQuantity" type="Insert">
  <binding name="value" expression="lineItem.quantity"/>
</component>

<component id="insertPrice" type="Insert">
  <binding name="value" expression="lineItem.price"/>
</component>

<component id="remove" type="ActionLink">
  <binding name="listener" expression="listeners.removeItem"/>
</component>

This is very important to the remove component. On some future request cycle, it will be expected to remove a specific line item from the shopping cart, but how will it know which one?

This is at the heart of the action service. One aspect of the IRequestCycle's functionality is to dole out a sequence of action ids that are used for this purpose (they are also involved in forms and form elements). As the ActionLink component renders itself, it allocates the next action id from the request cycle. Regardless of what path through the page's component hierarchy the rendering takes, the numbers are doled out in sequence. This includes conditional blocks and loops such as the Foreach.

The steps taken to render an HTML response are very deterministic. If it were possible to 'rewind the clock' and restore all the involved objects back to the same state (the same values for their instance variables) that they were just before the rendering took place, the end result would be the same. The exact same HTML response would be created.

This is similar to the way in which compiling a program from source code results in the same object code. Because the inputs are the same, the results will be identical.

This fact is exploited by the action service to respond to the URL. In fact, the state of the page and components is rolled back and the rendering processes fired again (with output discarded). The ActionLink component can compare the action id against the target action id encoded within the URL. When a match is found, the ActionLink component can count on the state of the page and all components on the page to be in the exact same state they were in when the page was previously rendered.

A small effort is required of the developer to always ensure that this rewind operation works. In cases where this can't be guaranteed (for instance, if the source of this dynamic data is a stock ticker or unpredictable database query) then other options must be used, including the use of the ListEdit component.

In our example, the remove component would trigger some application specific code implemented in its containing page that removes the current lineItem from the shopping cart.

The application is responsible for providing a listener method, a method which is invoked when the link is triggered.

Example 4.3. Listener method for remove component

public void removeItem(IRequestCycle cycle)
{
  getCart().remove(lineItem);
}

This method is only invoked after all the page state is rewound; especially relevant is the lineItem property. The listener gets the shopping cart and removes the current line item from it. This whole rewinding process has ensured that lineItem is the correct value, even though the remove component was rendered several times on the page (because it was wrapped by the Foreach component).

[Note]Listener Methods vs. Listener Objects

Listener methods were introduced in Tapestry 1.0.2. Prior to that, it was necessary to create a listener object, typically as an inner class, to be notified when the link or form was triggered. This worked against the basic goal of Tapestry: to eliminate or simplify coding. In reality, the listener objects are still there, they are created automatically and use Java reflection to invoke the correct listener method.

An equivalent JavaServer Pages application would have needed to define a servlet for removing items from the cart, and would have had to encode in the URL some identifier for the item to be removed. The servlet would have to pick apart the URL to find the cart item identifier, locate the shopping cart object (probably stored in the HttpSession) and the particular item and invoke the remove() method directly. Finally, it would forward to the JSP that would produce the updated page.

The page containing the shopping cart would need to have special knowledge of the cart modifying servlet; its servlet prefix and the structure of the URL (that is, how the item to remove is identified). This creates a tight coupling between any page that wants to display the shopping cart and the servlet used to modify the shopping cart. If the shopping cart servlet is modified such that the URL it expects changes structure, all pages referencing the servlet will be broken.

Tapestry eliminates all of these issues, reducing the issue of manipulating the shopping cart down to the single, small listener method.