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:
Gets the next value from the source
Updates the value into some property of its container
Renders its body
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).
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.