Advanced form components
4.4 Looping within a form using ListEdit
Dealing with lists is a common issue related to synchronization between the cli- ent and the server. Forms may contain a Foreach component and iterate over a list of values, allowing properties of each element in the list to be edited. This is common in e-commerce applications, where a “shopping cart” page allows quan- tities of all the items in the shopping cart to be edited in one place. The same concept applies in many other types of applications as well.
As you saw in chapter 3, it is easy for users to trip up the Form component by using their browser’s back button, triggering a StaleLinkException. For simple
forms, without loops or conditionals, this is never an issue. However, for complex forms, which include loops and conditionals, it is possible for a Form submission to get out of sequence with the previous Form render. The simplest way for a syn- chronization fault to occur is when the form includes a loop that iterates over data that may change between the time the page is rendered and the time the form on the page is submitted. This often occurs when a loop works off data from a database, and rows are added to, or removed from, the database after the page is rendered but before the form is submitted (for example, if a second user is updating the database). A similar scenario is one in which the user backtracks to a page that was rendered when the application was in a different state.
So, just as the Hidden component can store a single value in a form, the ListEdit component can store a list of values in a form—and iterate over them just like a Foreach component. The interface for ListEdit resembles that of the Foreach component; it also has source and value parameters. When a ListEdit is
rendering, it behaves exactly like the Foreach component, with one difference: It records a series of hidden fields in the form, one for each element in its source list. As with the Hidden component, it encodes each value, maintaining its type for later, when the form is submitted. Figure 4.6 shows how this works.
A major departure from the Foreach component is the existence of the ListEdit’s
listener parameter. Whereas a Foreach component updates its value parameter
and renders its body, the ListEdit component updates its value parameter and
then invokes a listener method before rendering its body. The intent is that the successive values are some form of object ID, and the listener method is sup- posed to read the full object for that ID and make it available as a page property. When the enclosing Form is rewinding, ListEdit works very differently from Foreach. During the rewinding, a ListEdit component reads the hidden fields it previously recorded (during the render); it doesn’t use its source parameter at
all when rewinding. The ListEdit component still updates the property bound to its value parameter, just as a Foreach component does, before rendering its
body. The operation sequence for rewinding is just about the same as for ren- dering (except for the interaction with the Form component to record hidden field values).
Figure 4.6
When rendering, the ListEdit component acts much like a Foreach. The listener is invoked after the value is updated on each pass through the loop.
Looping within a form using ListEdit 153 While the ListEdit component provides the functionality for iterating over lists, it is commonly combined with a helper class to fill in the details, as you’ll see in the next section.
4.4.1 Using the ListEditMap
The ListEdit component provides the raw structure for dealing with HTML form synchronization issues, but it requires a bit of effort to use. There’s a lot of work related to supplying ListEdit with a list or collection of IDs, converting those IDs to objects, and so forth. That can turn into a bit of code, so, naturally, Tapestry offers some assistance to help you avoid unnecessary coding.
The framework includes a companion class, ListEditMap, which provides a
server-side mapping from the object IDs that are stored in the form to the objects for those IDs, which are available on the server. A page must create a ListEditMap
instance and load it with the object IDs and matching object values before ren- dering the ListEdit component, and then must reconstruct the ListEditMap
instance when the form is submitted. The ListEdit component’s source and value parameters are connected to ListEditMap’s keys and key properties, as
shown in table 4.2.
With the ListEdit component properly configured, we can extend the ListEdit sequence diagram to show exactly how ListEditMap is involved, as shown in figure 4.7.
The listener method can get the current value from the ListEditMap instance.
This listener method should always check to see if the value is null; this can occur when two users’ updates overlap. If the first user deletes some objects, the second user’s form submit will contain the IDs of deleted objects, which will not be present in the ListEditMap instance (because they have been deleted from the
underlying database).
To see how ListEdit and ListEditMap work together, let’s update our ToDo
application again, replacing Foreach with a ListEdit component.
Table 4.2 ListEditMap properties
ListEdit parameter ListEditMap property Usage
source keys The collection of keys to iterate over (render only) value key A value set by ListEdit to the current key within the
list of keys (render and rewind)
value The value corresponding to the current key (render and rewind)
4.4.2 Using ListEdit in the ToDo application
In chapter 3, we demonstrated how easy it is to force the ToDo application (which is based on a Foreach component) to fail (with a StaleLinkException) by
using the browser’s back button. Now we’ll create a fourth version of the applica- tion that doesn’t have that limitation.
Creating a ToDo item with an ID
The ListEditMap class is built around the idea that items have some form of
unique key that can be used to identify them. In a production application, these
Figure 4.7 The ListEdit component is connected to the ListEditMap instance and keeps it informed about the current object ID (by setting the key property). The listener method can easily obtain the current object for that key from ListEditMap
Looping within a form using ListEdit 155 keys are likely to be some form of database primary key. The ToDoItems used in
these example applications are not stored in a database, but we can still create a kind of unique key for the items. The first step is to extend the ToDoItem class yet
again. Listing 4.6 shows the implementation of ToDoItem4, which extends from ToDoItem3 and adds a unique immutable identifier, as an int, to each item.
package examples.todo4;
import examples.todo3.ToDoItem3;
public class ToDoItem4 extends ToDoItem3 {
private static int _nextuid = 0; private int _uid = _nextuid++; public ToDoItem4()
{
super(); }
public ToDoItem4(String title) {
super(title); }
public int getUid() {
return _uid; }
}
A simple static variable is used to allocate unique IDs. In a real application, unique IDs would be provided by a database. This approach is acceptable only for this simple demonstration.
The unique ID is assigned as each instance is constructed.
Now that we have to-do items that can be identified using a unique ID, we can start updating the application to make use of the ListEdit component, starting with the page template.
Listing 4.6 ToDoItem4.java: data object for the ToDo4 page
Source of unique IDs
b
Assigns unique ID when object created
c
b
Updating the page template
The template for the ToDo4 page is for the most part identical to the ToDo3 page, except for the one line concerned with the item loop:
<tr jwcid="listEdit" element="tr">
Because the ListEdit component has even more parameters than the Foreach component used in the previous examples, it makes sense to configure those parameters in the ToDo4 page specification:
<component id="listEdit" type="ListEdit">
<binding name="source" expression="listEditMap.keys"/> <binding name="value" expression="listEditMap.key"/>
<binding name="listener" expression="listeners.synchronizeItem"/> </component>
Whereas the Foreach component in the previous to-do list examples read the
toDoList property and directly updated the item property, here the ListEdit
component reads and updates properties of the ListEditMap class. The item
property still gets updated, but indirectly, by the synchronizeItem() listener
method. The page is responsible for initializing the listEditMap property before
the page renders. The ListEditMap is initialized with the values from the toDo- List property (you’ll see the details of this shortly) and acts as a buffer between
the ListEdit component and the properties of the page.
In addition, the ToDo4 page may need to display an error message if a form submission cannot be processed; the HTML template includes a familiar pair of components for displaying the error message (if the message is non-null):
<span jwcid="@Conditional" condition="ognl:errorMessage"> <span style="{ font: bold; color: red }">
<span jwcid="@Insert" value="ognl:errorMessage">Error Message </span>
<br/> </span> </span>
As in previous examples, this HTML template snippet checks to see if there is a non-null error message and, if so, displays it at the top of the page, before the form renders.
Specifying the page properties
The ToDo4 page uses the same set of specified properties as the ToDo3 page, with two additions: a listEditMap property (connected to the ListEdit compo-
Looping within a form using ListEdit 157 when the form submission cannot be processed due to synchronization prob- lems). All of these properties are defined in the ToDo4 page specification:
<property-specification name="toDoList" type="java.util.List" persistent="yes"/> <property-specification name="item" type="examples.todo1.ToDoItem"/> <property-specification name="moveUpItem" type="examples.todo1.ToDoItem"/> <property-specification name="moveDownItem" type="examples.todo1.ToDoItem"/> <property-specification name="listEditMap" type="org.apache.tapestry.form.ListEditMap"/> <property-specification name="errorMessage" type="java.lang.String"/>
Now that we’ve specified the necessary properties, we need to make sure they are initialized and used properly.
Initializing the ListEditMap
Listing 4.7 contains the code for the ToDo4 page class. It extends the ToDo3 class,
adding support for initialization and using the ListEditMap instance.
package examples.todo4; import java.util.List; import org.apache.tapestry.IRequestCycle; import org.apache.tapestry.PageRedirectException; import org.apache.tapestry.event.PageEvent; import org.apache.tapestry.form.ListEditMap; import examples.todo1.ToDoItem; import examples.todo3.ToDo3;
public abstract class ToDo4 extends ToDo3 {
public abstract void setErrorMessage(String message); public abstract void setListEditMap(ListEditMap listEditMap); public abstract ListEditMap getListEditMap(); public abstract void setItem(ToDoItem item); protected ToDoItem createNewItem(String title) {
return new ToDoItem4(title); }
Listing 4.7 ToDo4.java: page class for the ToDo4 page
Accessors for specified properties
Creates new items as instances of ToDoItem4
public void pageBeginRender(PageEvent event)
{
super.pageBeginRender(event);
ListEditMap map = new ListEditMap();
List items = getToDoList();
int count = items.size();
for (int i = 0; i < count; i++)
{
ToDoItem4 item = (ToDoItem4) items.get(i); int uid = item.getUid();
map.add(new Integer(uid), item); }
setListEditMap(map); } public void synchronizeItem(IRequestCycle cycle)
{
ListEditMap map = getListEditMap();
ToDoItem item = (ToDoItem) map.getValue();
if (item == null)
{
setErrorMessage(
"Your form submission is out of date. Please retry."); throw new PageRedirectException(this);
}
setItem(item);
} }
The pageBeginRender() method will be invoked when the page renders or when
a form within the page rewinds. The super implementation must be invoked, because that ensures that the toDoList property is initialized.
Each item is added to the ListEditMap, which remembers the order in which key/
value pairs are added.
The ListEdit component invokes this listener method after setting ListEditMap’s key property. The map’s getValue() method returns the corresponding object,
as previously stored inside pageBeginRender(). If the current key is not stored in
the map, then null is returned—this can happen when a user submits a form after clicking the back button.
Invoked when page renders or when form rewinds
b
Sets up toDoList propertyAdds item to the ListEditMap
c
Stores the map for later Is invoked by ListEdit
d
➥
b
c
d
Looping within a form using ListEdit 159 The toDoList property is the list of items, in the correct order (the order as
manipulated by the user). ListEditMap remembers the order that items are
added to it; its getKeys() method returns the keys in that order. It is safe to cast
each item in the list to ToDoItem4, since the createNewItem() method has been
overridden to instantiate this class. Synchronizing the item
The second responsibility of the class is to synchronize the item property—the
ListEdit component stores a list of to do item IDs in the form, and the page class must use these to set the item property to the correct instance of ToDoItem4.
Nearly all the work for this is done by ListEditMap with a short listener method, synchronizeItem() (shown in listing 4.7), which gets the current item from the ListEditMap and assigns it to the page’s item property. In addition, the synchronize- Item() method includes a check for a null item.
As we’ve described earlier, the ListEdit component will set the key property of
the ListEditMap before invoking the listener method. Invoking getValue() on
the ListEditMap returns the corresponding value object, as previously recorded
into the map; this is an instance of ToDoItem4. The check for null covers race
conditions between two users,3 or one user using the browser’s back button.
Throwing a PageRedirectException aborts the form’s rewind phase entirely and
forces the page to render as is.
Chapter 10 describes some further functionality provided by the ListEditMap
class—specifically, the ability to track values within the map that should be deleted. The ToDo application doesn’t use this feature of ListEditMap, because it
has its own approach for deleting completed items in the list.
The previous two sections described components, Hidden and ListEdit, that are critical to the infrastructure of an application but largely invisible to end users. Let’s now return to components that users will see and interact with.
3 A race condition is a category of software bug that is concerned with multiple users affecting the same
data simultaneously. In a race condition, two (or more) users attempt the same operation at roughly the same time, and the first user to complete the operation affects the outcome of the other users. A common example of a race condition in a web application is when one user deletes an object stored in a database and another user attempts to update the same object. Whether the second user’s update succeeds or fails depends on which user wins the race.