CVS: TapestryBook/doc Chapter-04-AdvancedForms.doc,NONE,1.1 index.html,1.6,1.7 Chapter-03-Forms.doc,1.5,1.6
Howard Lewis Ship <[email protected]> Tue, 04 Mar 2003 08:43:38 -0800
| Newsgroups | gmane.comp.java.tapestry.cvs |
|---|---|
| Message-ID | <[email protected]> |
Update of /cvsroot/tapestry/TapestryBook/doc
In directory sc8-pr-cvs1:/tmp/cvs-serv23946/doc
Modified Files:
index.html Chapter-03-Forms.doc
Added Files:
Chapter-04-AdvancedForms.doc
Log Message:
Split chapter 3 into two chapters and make a number of corrections.
--- NEW FILE: Chapter-04-AdvancedForms.doc ---
ÐÏࡱá
label="Move Down"/></td>
</tr>
</table>
<input type="submit" value="Update"/>
<input type="submit" jwcid="@Submit"
listener="[[ listeners.addTodoItem ]]"
label="Add Item"/>
<input type="submit" jwcid="@Submit"
listener="[[ listeners.deleteCompleted ]]"
label="Delete Completed"/>
</form>
<hr/>
<p><a href="#" jwcid="@PageLink" page="Home">Return to
Home page</a>.</p>
</body>
</html>
(annotation) <#1 The major change is adding this component to edit the priority property of the item.>
The bulk of the template is an exact copy of the original ToDo template. What's added is the PropertySelection component. Its value parameter is bound to the property to edit, just as with a TextField. Unlike a TextField, the property may be of any type, not just string.
4.1.3 Implementing the Page Class
The majority of the ToDo2 class is the same as for the original page, ToDo. The differences are:
ToDo2 provides a read-only property, priorityModel, needed by the PropertySelection component.
When adding new items, instantiate class ToDoItem2 instead of ToDoItem.
Since so much is common, class ToDo2 subclasses the original class, ToDo, overriding two methods and adding a third. First, the model property is provided by the getPriorityModel() method.
public IPropertySelectionModel getPriorityModel()
{
return new PriorityModel();
}
New item instances are created in only two places, when adding a new item, and when creating the initial list of items. New items are created in the addToDoItem() listener method. ToDo2 simply replaces the implementation provided by class ToDo.
public void addTodoItem(IRequestCycle cycle)
{
getToDoList().add(new ToDoItem2("New Item"));
}
In addition, the two initial items are added in beginResponse(). This method is also overridden in ToDo2.
public void beginResponse(IMarkupWriter writer,
IRequestCycle cycle)
throws RequestCycleException
{
List list = getToDoList();
if (list == null)
{
list = new ArrayList();
list.add(new ToDoItem2("Finish reading Tapestry Book"));
list.add(new ToDoItem2("Download latest version of Tapestry"));
setToDoList(list);
}
}
All the remaining behavior from the first page applies to this page as well, and is simply inherited from the original page class.
4.1.4 Implementing the Model
The model is a class which implements the IPropertySelectionModel interface, which consists of five methods.
When rendering, the PropertySelection creates a <select> element, then uses the model to get the number of options. For each option, it gets the option value, the string value and the label, then uses this information to write an <option> element. The option value is used to see if the <option> should be selected by default. This sequence is illustrated in figure 04-xx-PSRender.
Figure 04-xx-PSRender: The property selection model provides the number of options, and the information needed to render each individual option.
The first method provides the number of items in the list.
public int getOptionCount()
{
return 3;
}
For this model, there are always exactly three options: "High", "Medium" and "Low". The PropertySelection component will iterate through the possible indexes (0 through 2) and invoke the next three methods for each index.
public Object getOption(int index)
{
switch (index)
{
case 0 :
return new Integer(ToDoItem2.HIGH_PRIORITY);
case 1 :
return new Integer(ToDoItem2.MEDIUM_PRIORITY);
default :
return new Integer(ToDoItem2.LOW_PRIORITY);
}
}
public String getLabel(int index)
{
switch (index)
{
case 0 :
return "High";
case 1 :
return "Medium";
default :
return "Low";
}
}
The first method, getOption(), is used when rendering; it allows the PropertySelection component to determine which option, if any, should be initially selected. The first option whose option value, as returned by getOption(), matches the value from the value parameter is selected (the <option> element will include the "selected" attribute). Keep in mind that, although this example edits an integer property, PropertySelection has no knowledge of the type of property it is editing; that information is entirely contained within the model. The value parameter could just as easily be a double, or a string, or a Date, or some other object entirely. The return value is Object, and the three constants are wrapped as Integer objects and returned.
The second method, getLabel(), provides the label for each option. As with getOption(), the parameter is the index within the loop. The returned label will be presented directly to the user. In an internationalized application, the label value returned must be localized to match the page's locale.
As the PropertySelection components renders the <option> elements, it must have a string to use as the value attribute. Rather than mandate a value, the PropertySelection allows the model to specify this, in the getValue() method. A common approach, used here, is to simply encode the index as a string. What's important is that the model should be able to convert this string back into an option value when the form is submitted.
public String getValue(int index)
{
return Integer.toString(index);
}
In this particular model, another option would have been to encode the value for one of the three constants here, rather than the index. That is, return "100", "200" or "300".
The final method is only used when the form is submitted. It is used to directly translate from an encoded value to an option, so that the option can be be assigned to the PropertySelection's value parameter. This is only used when the PropertySelection component is rewinding, a shown in the figure 04-xx-PSRewind.
Figure 04-xx-PSRewind: PropertySelection Rewind Sequence. The value is extracted from the query parameter and the model is used to translate it into a value which is assigned back to the domain object property.
public Object translateValue(String value)
{
int index = Integer.parseInt(value);
return getOption(index);
}
For this model implementation, it was convenient to simply convert the index value back to an int, and use the getOption() method to get the value to be assigned. Another option would have been to have the value simply be the string representation of the priority value (100, 200 or 300). In that case, translateValue() could simply create an Integer from the value parameter and return that as the option value.
4.1.5 Using Enums instead of Integers
Using a set of integers to represent the different priority levels is somewhat of an oddity in Java coding style. In C or C++, we would almost certainly use an enum instead, but Java does not have such a construct. Sun may eventually add some form of enumerated type to the Java Specification, but in the meantime, we can simulate C enums using Java classes.
In this final take on the ToDo list application, we'll switch from representing the priority as integers and instead use an enumerated type. We'll also make use of a framework provided model class, tailored for use with enumerated types. The final application looks identical to the previous version, ToDo2. It's simply a different way of storing data on the server side.
One of the advantages to building and using open-source software is the availablility of other high quality frameworks. Tapestry makes use of a number of such frameworks, including the OGNL library, but also including another framework available from Jakarta, the Jakarta Commons. Commons includes a number of handy utilities and classes, one of which is a Enum base class, used for creating enumerated types. Creating a new enumerated type is as simple as creating a subclass of Enum and defining some public constants for the possible values, as shown in listing 04-xx.
Listing 04-XX: Priority.java
package examples.todo3;
import org.apache.commons.lang.enum.Enum;
public class Priority extends Enum
{
public static final Priority HIGH = new Priority("HIGH");
public static final Priority MEDIUM = new Priority("MEDIUM");
public static final Priority LOW = new Priority("LOW
");
public static final Priority[] ALL_VALUES =
new Priority[] { HIGH, MEDIUM, LOW };
private Priority(String name)
{
super(name);
}
}
The constructor for Priority is private; this means that the class can't be extended. There's no way to create additional instances of Priority beyond the three values defined here: Priority.HIGH, Priority.MEDIUM and Priority.LOW (except by changing the Priority class to define new values). If we ever write code that needs to differentiate ToDoItem3 instances based on priority, we can make direct comparisons to these three constant values; that is, we can use the Java equality operator, "==", instead of invoking the equals() method. The significant part of the Enum class is ensuring identity, even when a Priority is serialized and deserialized.
From there, we define a priority property on the latest version of the ToDoItem class, ToDoItem3, shown in listing 04-xx-ToDoItem3.
Listing 04-xx-ToDoItem3: ToDoItem3.java
package examples.todo3;
import examples.todo1.ToDoItem;
public class ToDoItem3 extends ToDoItem
{
private Priority _priority = Priority.MEDIUM;
public ToDoItem3()
{
}
public ToDoItem3(String title)
{
super(title);
}
public Priority getPriority()
{
return _priority;
}
public void setPriority(Priority priority)
{
_priority = priority;
}
}
The HTML template for page ToDo3 is identical ToDo2 (except for the page title). There's nothing to change; the PropertySelection is still editing the priority property of the item, and the page still provides an instance of IPropertySelectionModel to handle all the translations. On the implementation side, the type of the priority property has changed from int to Priority, and the model used by the PropertySelection component has changed as well, but none of that is relevant to the interface. This is a good example of the power of the Model View Controller pattern: The view (the template) is unchanged even when the Model (the ToDoItem3 class) is changed; the Controller has modest changes to fullfil its role of bridging the two.
The ToDo3 page specification differs only in the page class to instantiate. The class itself has two changes: the methods which create new items now instantiate ToDoItem3 instead of ToDoItem2. The only real change is related to creating a model for the PropertySelection.
private IPropertySelectionModel _priorityModel;
public IPropertySelectionModel getPriorityModel()
{
if (_priorityModel == null)
_priorityModel = buildPriorityModel();
return _priorityModel;
}
private IPropertySelectionModel buildPriorityModel()
{
ResourceBundle bundle =
ResourceBundle.getBundle("examples.todo3.PriorityStrings",
getLocale());
return new EnumPropertySelectionModel(Priority.ALL_VALUES,
bundle);
}
The core of this is the framework class EnumPropertySelectionModel. This is an implementation of IPropertySelectionModel geared around Enums. The first parameter for the constructor is a list of the Enum values, in the order in which they should appear in the drop down list created by the PropertySelection component.
The second parameter is a ResourceBundle containing the localized labels to use. The keys in the ResourceBundle are the names of the Enum instances, in this case "HIGH", "MEDIUM" and "LOW". The file PriorityStrings.properties contains the user-presentable labels:
HIGH=High
MEDIUM=Medium
LOW=Low
Tapestry does quite a bit in terms of managing the end-user's locale; this is covered in detail in chapter XXX. For now, suffice to say that the page knows the correct locale for any localizable resources such as the PriorityStrings.properties file.
The PropertySelection component vastly simplifies the effort needed to create a drop-down list. This single component can be used to create lists for anything from a simple "Yes" or "No", to a list of strings, all the way up to a list of entity objects read from a database; all it requires is to be provided with the correct model for what the data is, how to store it into the HTML form, and how to convert it back to application data.
As indispensible as the PropertySelection component is, it is still necessary to accept user input typed by the user, not selected from a list, and allow that input to be validated. The next section introduces the input validation subsystem organized around the ValidField component.
4.2 Validating User Input with ValidField
Tapestry includes an entire substem related to validating user input. This subsystem allows:
Formatting of non-string values into strings (for a field's default value).
Parsing of strings into objects (on form submission).
Tracking of errors on a field-by-field basis.
Visible decorations of fields and labels that have errors.
Client-side validation.
Central to this subsystem is the ValidField component, which is a variation on the TextField component. Validation in Tapestry is not merely about checking that the text typed by the user is correct; it is part of a larger usability picture. Users should not be scolded about input mistakes; they should be directed to enter correct values. They should never be forced to re-enter invalid input, instead they should be guided on how to correct the invalid input. They should be given good visual feedback about any errors in a form, and guided to make corrections with a minimum number of key strokes and mouse motions. Finally, as much as possible, they should make corrections on the client side, without requiring a round trip to the server. The validation subsystem provides all the facilities needed to achieve this level of usability in a general fashion, and is
ValidField components are capable of editing not just string properties, but dates and numbers as well. The validations that can be used with ValidField components can apply both client-side and server-side checks on input. These checks allow fields to be required, or force user input to be within a specified range. In many cases, the validations are typed in to conversions, such as converting input from a string to a Date or some type of number.
An example of ValidField in use is shown in figure 04-xx-RegistrationStart. This is a page that accepts a user's name and address. Initially, all the fields are blank, and the cursor is automatically placed into the first required field (using a snippet of client-side JavaScript).
Figure 04-xx-RegistrationStart: The Registration page in initial state. Each required field is marked with an asterisk, and the cursor is placed into the first required field.
Entering values into some fields but not others, and submitting the form results in a much different display, as shown in figure 04-xx-RegistrationError. Details about the first field error are displayed up top; each field is marked in error in several ways: the label and the field itself change color, and an error marker is added to each field.
Figure 04-xx-RegistrationError: The same form partially filled out and submitted. The first error on the page is displayed prominently. All fields with errors are highlighted in three ways: the field label text is red, the field itself gets a red background, and an icon is displayed to the right of the field.
So, how does this all work? It's more than just a single component in isolation can accomplish; there are significant changes to the output HTML scattered thoughout the page. Making this work requires one additional component (FieldLabel) and two additional objects (the validator, and the validation delegate) and some complex interactions between all four.
4.2.1 Using FieldLabels in conjunction with ValidFields
A FieldLabel is a companion component to a ValidField. Each FieldLabel is connected to ValidField via the FieldLabel's field parameter. The FieldLabel can adjust its visual look to reflect the field: if the field is in error (because of invalid user input), the FieldLabel can display itself differently. Figure 04-xx-RegistrationError shows this; the labels on several fields have been turned red.
In addition, the FieldLabel takes the name of the field to display from the field itself (ValidField has a displayName par
ameter used for this purpose). This ensures that the field label matches the name of the field used in any error messages, even when the name of the field is localized (or otherwise variable).
4.2.2 Understanding Validators
Validators are simply objects that implement the interface IValidator. A validator has four responsibilities:
Converting an object value to a string that can be used when rendering the page. The converted value is used as the value attribute of the <input> element for the ValidField.
Converting a string value submitted with the form back into an object value.
Performing additional validations on the input.
Writing any client-side JavaScript needed to perform client-side validations.
Each ValidField component has a validator parameter, which will be a validator object for that field. Validator objects are meant to be shared; they don't store any information about a particular ValidField. Many ValidFields may share the same validator.
All validators include a required property. Required is a boolean property, when set to true a validator will not allow an input field to be blank, or to consist only of whitespace.
Tapestry provides a number of implementations of the IValidator interface that can be used and configured off the shelf. StringValidator is used for editing string properties of pages or domain objects, and can apply an additional validation: a minimum number of characters to accept.
NumberValidator is used for editing numeric properties of all types (int, long, BigDecimal, etc.). Number validator can enforce a minimum or a maximum value, or both. DateValidator is used for editing Date properties. Like NumberValidator, a minimum or a maximum value may be set. Later in this section, we'll see how to create a custom validator, one that enforces the format of a postal zip code (as shown in figure 04-xx-RegistrationZip).
Figure 04-xx-RegistrationZip: A custom validator attached to the Zip field knows the correct format for zip codes and can generate a custom error message.
4.2.3 Using Validation Delegates
The validation delegate has two functions, mixed together. First and foremost, it tracks the error state of each ValidField enclosed by a Form. When the form is submitted, each ValidField passes the string provided by the user to the validator. The validator may convert the string to an object type such as Long or Double, or leave it as a string. It will also apply any validations, such as checking that the converted value fits into a specified range. If the converted value fails a validation, the validator throws an exception. The conversions and validations a validator can perform are flexible and, as we'll see, it is very easy to create new validators to handle new conversions and validations.
When a validation exception is thrown by a validator, the ValidField catches the exception and uses the validation delegate to record the ValidField's element id, the exception message and the invalid input provided by the user. This is the indicator that the field is in error.
Validation delegates have a second, discrete function. The delegate is responsible for decorating the fields and labels that are in error. This is accomplished by writing additional attributes into the <input> element rendered by the ValidField. The validation delegate also has opportunities to render additional HTML before and after the FieldLabel renders, and before and after the ValidField renders. Figure 04-xx-LabelRender illustrates how the delegate is hooked into the rendering of the FieldLabel. This additional rendering is how the labels for invalied fields manage to be displayed in red: the validation delegate has a chance to wrap the FieldLabel's output in a <span>. The delegate also has a chance to write a class attribute into the <input> tag, and a chance to write a snippet of HTML to indicate which fields are required and which are in error.
Figure 04-xx-LabelRender: The field gets the user-presentable name for the field from the ValidField component. It allows the delegate to render before and after it renders the field name, so that the delgate to decorate the label if the corresponding field is in error.
Likewise, both the validator and the validation delegate are integrated into the render of the ValidField, as showin in figure 04-xx-ValidFieldRender.
Figure 04-xx-ValidFieldRender: The delegate and the validator are both hooked into the rendering of the ValidField. The ValidField uses its hook to create client-side validation JavaScript. The delegate uses its hooks to decorate fields that are in error.
4.2.4 Building the Registration Page
The screenshots in this section are of a simple registration page. The ultimate aim of the Registration page is to collect a user's name and address and store it into a Java object. The class, Address, is provided in listing 04-xx-Address.
Listing 04-xx-Address: Address.java
package examples.register;
import java.io.Serializable;
public class Address implements Serializable
{
private String _firstName;
private String _lastName;
private String _address1;
private String _address2;
private String _city;
private String _state;
private String _zip;
public String getAddress1()
{
return _address1;
}
public String getAddress2()
{
return _address2;
}
public String getCity()
{
return _city;
}
public String getFirstName()
{
return _firstName;
}
public String getLastName()
{
return _lastName;
}
public String getState()
{
return _state;
}
public String getZip()
{
return _zip;
}
public void setAddress1(String address1)
{
_address1 = address1;
}
public void setAddress2(String address2)
{
_address2 = address2;
}
public void setCity(String city)
{
_city = city;
}
public void setFirstName(String firstName)
{
_firstName = firstName;
}
public void setLastName(String lastName)
{
_lastName = lastName;
}
public void setState(String state)
{
_state = state;
}
public void setZip(String zip)
{
_zip = zip;
}
}
The first step in creating this page is the HTML template, show in listing 04-xx-Registration.
Listing 04-xx-Registration: Registration page HTML template
<html jwcid="@Shell" title="Registration"
stylesheet="[[ assets.stylesheet ]]">
<head jwcid="$remove$">
<link rel="stylesheet" type="text/css" href="css/style.css"/>
</head>
<body jwcid="@Body">
<span class="title">Registration</span>
<p>Please enter your mailing address for our records.
Fields marked with a
<span class="required-marker">*</span>
are required.
</p>
<span jwcid="@Conditional"
condition="[[ beans.delegate.hasErrors ]]">
<table class="error">
<tr valign="top">
<td>
<IMG height="52" alt="[Error]" src="images/form-error.png"
width="52">
</td>
<td>
<span jwcid="@Delegator"
delegate="[[ beans.delegate.firstError ]]">
Error Message
</span>
</td>
</tr>
</table>
</span>
<form jwcid="@Form"
listener="[[ listeners.formSubmit ]]"
delegate="[[ beans.delegate ]]">
<table class="form">
<tr>
<th><span jwcid="@FieldLabel"
field="[[ components.inputFirstName ]]">First Name
</span></th>
<td><input type="text" jwcid="inputFirstName" size="50"/></td>
</tr>
<tr>
<th><span jwcid="@FieldLabel"
field="[[ components.inputLastName ]]">Last Name
</span></th>
<td><input type="text" jwcid="inputLastName" size="50"/></td>
</tr>
<tr>
<th><span jwcid="@FieldLabel"
field="[[ components.inputAddress1 ]]">Address
</span></th>
<td><input type="text" jwcid="inputAddress1" size="50"/></td>
</tr>
<tr>
<td></td>
<td><input type="text" jwcid="@TextField" value="[[ address.address2 ]]" size="50"/></td>
</tr>
<tr>
<th><span jwcid="@FieldLabel"
field="[[ components.inputCity ]]">City
</span></th>
<td><input type="text" jwcid="inputCity" size="50"/></td>
</tr>
<tr>
<th><span jwcid="@FieldLabel"
field="[[ components.inputState ]]">State
</span></th>
<td><input type="text" jwcid="inputState" size="2"/></td>
</tr>
<tr>
<th><span jwcid="@FieldLabel"
field="[[ components.inputZip ]]">Zip
</span>
</th>
<td><input type="text" jwcid="inputZip" size="10"/>
</tr>
<tr>
<td></td>
<td><input type="image" src="images/continue.png"
width="100" height="32"/>
</tr>
</table>
</form>
<hr/>
<p><a href="#" jwcid="@PageLink"
page="Home">Return to Home page</a>.</p>
</body>
</html>
This template introduces a wealth of new concepts, so we'll take it apart a little piece at a time.
Defining a Stylesheet for the Page
The Registration page makes heavy use of Cascading Style Sheets; that's how fields and labels turn red when they are in error. To support this, the rendered page must include that stylesheet. Stylesheets are included using a <link> element, within the <head> element (within the <html> element). Since the Shell component is writing the <html> and <head> elements, it is necessary that the Shell component write the link to the stylesheet; this is accomplished by binding its stylesheet parameter to an asset. A declaration for that asset will appear in the Registration page's specification.
<html jwcid="@Shell" title="Registration"
stylesheet="[[ assets.stylesheet ]]">
Now comes a conundrum: we still would like the Registration page to preview properly while editing
but if the template includes the <head> and <link> elements needed to include the stylesheet, then the final rendered page will include two sets of <head> and <link> elements: one from the template, one dynamically rendered by the Shell component.
Tapestry includes a little trick to sidestep this issue:
<head jwcid="$remove$">
<link rel="stylesheet" type="text/css" href="css/style.css"/>
</head>
That special component id, "$remove$" is the key. It's not normally a valid id (because it contains a dollar sign). However, Tapestry allows it as a special case, but it doesn't define a new component. Instead, cues Tapestry to removes the element and everything enclosed by the element, as if it was never in the template in the first place. With this in place, the page previews correctly when editing, as shown in figure 04-xx-RegistrationPreview.
Figure 04-xx-RegistrationPreview: The Registration page still previews properly in an HTML aware editor.
Declaring the Body Component
The ValidField component will almost always produce some client-side JavaScript. At the very least, initialization code is included which moves the cursor to the first field that is either required (but empty) or in error. Remember that the use of the Body component is required if any components on the page will produce any JavaScript. Failure to provide the Body component will result in a runtime error. Earlier we said that you should always make use the Body component in your pages, ValidField is an example of why.
This template includes a proper declaration for a Body component.
<body jwcid="@Body">
Displaying Validation Errors
The next section of the template is concerned with displaying validation errors. The validation delegate for the page is obtained and checked to see if it contains any errors. This hasErrors flag will always be false when a page is initially rendered. The hasErrors flag is set during the Form's rewind, when a form submission takes place. As we'll see, the Form's listener method will also query the delegate's hasErrors property. If the property is false, it is safe to take the validated input and move forward to the next step in the process. If hasErrors is true, the Registration page is redisplayed, to display the errors and decorate any fields that are in error.
Although it is possible to display all error messages for all fields that are in error, such output would be unwieldy on large forms and not very useful. Instead, the error message for the first field that is in error is displayed, but all fields throughout the form that are in error are marked. We've seen examples of this in figure 04-xx-RegistrationError, where the message refers to only the first field in error, even though several fields are marked.
<span jwcid="@Conditional"
condition="[[ beans.delegate.hasErrors ]]">
<table class="error">
<tr valign="top">
<td>
<IMG height="52" alt="[Error]" src="images/form-error.png"
width="52">
</td>
<td>
<span jwcid="@Delegator"
delegate="[[ beans.delegate.firstError ]]">
Error Message
</span>
</td>
</tr>
</table>
</span>
In this example, the validation delegate is obtained using the OGNL expression "beans.delegate". The Conditional component queries the delegate's hasErrors property and displays the error message (formatted inside a <table>) only if true. We also use a new component, Delegator. A Delegator component is a kind of universal hook used when rendering. It delegates to a second object, an object that implements the IRender interface (the IComponent interfaces extends the IRender interface, so all components are, naturally, renderable). This is very powerful, because the Delegator's delegate has complete freedom to write not just text, but full HTML.
The errors that the validation delegate returns are not simply strings, but objects that implement the IRender interface. Because these are objects and not strings, we can't simply use the Insert component. Instead, we use the Delegator component, which invokes the render() method on the renderable object provided to it.
The framework validator classes don't take great advantage of this facility; the error messages could just as easily be strings. However, your custom applications may include validators where the error message contains a mixture of text and HTML tags.
The validation delegate, as a convenience, has a firstError property that is the renderable object for just the first field error. The firstError property is passed to the Delegator, which results in the error message being displayed.
Starting the Form
The Form component is used as before with one addition. An additional parameter, delegate, is specified to link the Form to the validation delegate. Every FieldLabel and ValidField component enclosed by the Form must use the same validation delegate, and they all will
since they all will retrieve the validation delegate through the Form.
<form jwcid="@Form"
listener="[[ listeners.formSubmit ]]"
delegate="[[ beans.delegate ]]">
Again, the OGNL expression "beans.delegate" points to shared validation delegate.
Using a FieldLabel
Each ValidField will be preceded in the HTML template by a FieldLabel. The FieldLabel is connected to its partner ValidField by the FieldLabel's field parameter.
<span jwcid="@FieldLabel"
field="[[ components.inputFirstName ]]">First Name
</span>
The OGNL expression "components.inputFirstName" is a reference to the inputFirstName component of the page. Every page (in fact, every component) provides a read-only Map of all the components it contains. The keys of this Map are the component ids. Using OGNL, we can access the values in a Map just as easily as the properties of an ordinary JavaBean. Of course, to build a reference, we must know the id of the component, which is one reason the ValidField components are given explicit ids.
During the render, the FieldLabel discards its body (the text "First Name") and gets, from the ValidField, the correct field name to display as a label. This may seem cumbersome, but it is useful for two reasons. First, it ensures that the label in the HTML template matches the name for the field used in an error messages generated by the ValidField's validator. Secondly, if the ValidField localizes the name, the FieldLabel will still match. In addition, the validation delegate will have a chance to render before and after the FieldLabel; this allows the delegate to decorate the label when the field is in error.
Using a ValidField
In this example, each ValidField is constructed as a declared component, not an implicit component. Each ValidField appears in the HTML template, but its type and most of its parameters are declared in the page's specification. The ValidFields may not be anonymous, they must have real ids. This is necessary so that the placeholder in the template can be linked to the declaration in the page specification, but also so that the FieldLabel can be connected to the ValidF
ield.
Each ValidField appears in the template minimally, because the bulk of the component's configuration is in the page specification.
<input type="text" jwcid="inputLastName" size="50"/>
Understanding the Page Specification
The page specification for the Register page is shown in listing 04-xx-Register. It has two main sections (beyond the elements we've seen before, such as defining the page class, and declaring assets). The first section defines additional helper beans used with this page, including the validation delegate and the validators used by the ValidField components. The second section defines each of the ValidField components.
Listing 04-xx-Register: Register.page
<?xml version="1.0"?>
<!-- $Id: Login.page,v 1.1 2003/02/20 15:13:38 hship Exp $ -->
<!DOCTYPE page-specification PUBLIC
"-//Apache Software Foundation//Tapestry Specification 1.4//EN"
"http://jakarta.apache.org/tapestry/dtd/Tapestry_1_4.dtd">
<page-specification class="examples.register.Register">
<bean name="delegate"
class="examples.register.RegisterDelegate"/>
<bean name="required"
class="net.sf.tapestry.valid.StringValidator"
lifecycle="page">
<set-property name="required" expression="true"/>
</bean>
<bean name="stateValidator"
class="net.sf.tapestry.valid.StringValidator"
lifecycle="page">
<set-property name="required" expression="true"/>
<set-property name="minimumLength" expression="2"/>
</bean>
<bean name="zipValidator"
class="examples.register.PatternValidator"
lifecycle="page">
<set-property name="pattern">
"\\d{5}(-\\d{4})?"
</set-property>
<set-property name="errorMessage">
"Zip code format is five or nine digits.
Example: 02134 or 02474-1145."
</set-property>
</bean>
<component id="inputFirstName" type="ValidField">
<static-binding name="displayName" value="First Name"/>
<binding name="validator" expression="beans.required"/>
<binding name="value" expression="address.firstName"/>
</component>
<component id="inputLastName" type="ValidField">
<static-binding name="displayName" value="Last Name"/>
<binding name="validator" expression="beans.required"/>
<binding name="value" expression="address.lastName"/>
</component>
<component id="inputAddress1" type="ValidField">
<static-binding name="displayName" value="Address"/>
<binding name="validator" expression="beans.required"/>
<binding name="value" expression="address.address1"/>
</component>
<component id="inputCity" type="ValidField">
<static-binding name="displayName" value="City"/>
<binding name="validator" expression="beans.required"/>
<binding name="value" expression="address.city"/>
</component>
<component id="inputState" type="ValidField">
<static-binding name="displayName" value="State"/>
<binding name="validator" expression="beans.stateValidator"/>
<binding name="value" expression="address.state"/>
</component>
<component id="inputZip" type="ValidField">
<static-binding name="displayName" value="Zip"/>
<binding name="validator" expression="beans.zipValidator"/>
<binding name="value" expression="address.zip"/>
</component>
<context-asset name="stylesheet" path="css/style.css"/>
</page-specification>
This specification introduces two new elements, <bean> and <set-property>. The <bean> element is used to define a helper bean, an additional object available to the page that is responsible for some of the page's processing. The <set-property> element, enclosed within a <bean> element, is used to initialize a helper bean, setting a named property of the bean to the value of an OGNL expression.
Helper beans are very powerful concept, because they are a way to easily extend the functionality of an application through aggregation instead of inheritance. The most common use for beans is as shown here, defining validation delegates and validators to support ValidField components, but there is no reason to stop there. We've already seen the power of aggregation elsewhere; the use of components throughout Tapestry is one example of aggregation. Helper beans extend this concept further, allowing the logic of the application to be assembled from components, not just the presentation.
Once a helper bean is defined, it is accessed via the beans property of the page, as shown in the template. For example, the "delegate" bean can be referenced using the OGNL expression "beans.delegate".
Defining The Validation Delegate
The most basic use of the <bean> element is shown in the definition of the "delegate" bean. Just the name of the bean, and the class to instantiate are provided.
<bean name="delegate"
class="examples.register.RegisterDelegate"/>
Like any JavaBean, a helper bean must have a public constructor that takes no arguments. Tapestry will create the helper bean as needed, the first time it is referenced. By default, the helper bean is retained until the end of the current request. At the end of the request, the bean is discarded and will eventually be destroyed by the Java garbage collector.
The RegisterDelegate class is an implementation of the IValidationDelegate interface that customizes the rendering hooks of the delegate to reference CSS styles and images specific to the Register application. We'll be examining its code in detail shortly.
Defining Validators
The majority of the fields in the form are used to edit simple string properties, and the only validation constraint applied to them is that a non-null value be supplied by the user. All such ValidField components will share a bean named "required" as their validator.
<bean name="required"
class="net.sf.tapestry.valid.StringValidator"
lifecycle="page">
<set-property name="required" expression="true"/>
</bean>
This bean instantiates an instance of StringValidator and sets the required property of the StringValidator to true. In addition, it changes the lifecycle of the bean from the default ("request") to "page". A helper bean with a lifecycle of "page" is not discarded at the end of the request cycle. Once created, the bean is retained for as long as the page instance exists, and will be used again and again. This is appropriate for an instance of StringValidator, because StringValidator has no internal state (beyond the configuration of its required property, which never changes).
The "delegate" bean should not be given a lifecycle of "page" because it has a considerable amount of internal state (the error messages for fields that are in error) that is only relevant to a single request of a single user.
The ValidField for the state property uses a different validator, "stateValidator".
<bean name="stateValidator"
class="net.sf.tapestry.valid.StringValidator"
lifecycle="page">
<set-property name="required" expression="true"/>
<set-property name="minimumLength" expression="2"/>
</bean>
Like the "required" bean, the "stateValidator" bean is required. It adds a second constraint, configured through a second property, requiring the input to be at least two characters in length.
The final validator is for the zip code field. Zip codes have a pattern that is best described using a regular expression. Tapestry doesn't provide a validator along these lines, but it is easy enough to create one. Although we could simply create a validator that only supports the zip code pattern, it is virtually no extra work to create a flexible validator where both the pattern to check against, and the error message to display if the input fails to match the pattern, are configurable.
In terms of maximizing usability, it is very important that error messages be as helpful as possible. In the PatternValidator, simply telling the user that their input didn't match a regular expression would leave users frustrated and at a loss on how to fix it. Instead, a custom error message can be provided that tells the user exactly what they need to do. For the zip code field, the error message is customized to give the user examples of the two zip code formats accepted.
<bean name="zipValidator"
class="examples.register.PatternValidator
"
lifecycle="page">
<set-property name="pattern">
"\\d{5}(-\\d{4})?"
</set-property>
<set-property name="errorMessage">
"Zip code format is five or nine digits.
Example: 02134 or 02474-1145."
</set-property>
</bean>
Here we are using an alternate usage of the <set-property> element. Instead of specifying an expression attribute, as in the previous examples, we are putting the OGNL expression in the body of the <set-property> element. This is useful here, because we must enclose the literal string values (the pattern and the error message) in double quotes. Putting the OGNL expression in the body is much easier when the expression is long or contains complex punctuation, such as a mix of single and double quotes.
Declaring the ValidFields
All the ValidField component declarations follow the same general template. A display name for the field is defined; this is used by the FieldLabel, and in any error messages created by the field's validator. Like an ordinary TextField component, the value parameter is bound to the property the ValidField will edit. Lastly, a validator (different for each ValidField) is specified.
<component id="inputFirstName" type="ValidField">
<static-binding name="displayName" value="First Name"/>
<binding name="validator" expression="beans.required"/>
<binding name="value" expression="address.firstName"/>
</component>
The inputFirstName component uses the "required" bean as its validator and edits the firstName property of the address.
Implementing the Register Page
The Java code for the Register page is concerned with providing the address property referenced by the many ValidField components, and handling the form submission. It is provided in listing 04-xx-RegisterSource.
Listing 04-xx-RegisterSource: Register.java
package examples.register;
import net.sf.tapestry.IRequestCycle;
import net.sf.tapestry.html.BasePage;
import net.sf.tapestry.valid.IValidationDelegate;
public class Register extends BasePage
{
private Address _address;
public void initialize()
{
_address = null;
}
public Address getAddress()
{
if (_address == null)
_address = new Address();
return _address;
}
public void formSubmit(IRequestCycle cycle)
{
IValidationDelegate delegate =
(IValidationDelegate) getBeans().getBean("delegate");
if (delegate.getHasErrors())
return;
RegisterConfirm next =
(RegisterConfirm) cycle.getPage("RegisterConfirm");
next.setAddress(getAddress());
cycle.setPage(next);
}
}
The address property is lazily created as needed. This could also have been accomplished using either a <property-specification> element in the page specification, or by create a helper bean, using a <bean> element in the specification. The initialize() method is called at the beginning and end of the request, to return the page to its newly-initialized state (this is required for page instance pooling, a subject covered in detail in chapter XX).
The listener method, formSubmit(), is invoked when the Form is submitted. In order to determine if there were any errors, the validation delegate is accessed. If there are errors, the method simply returns, causing the Registration page to redisplay with the error message shown and invalid fields highlighted.
It is also possible to perform additional form-level validation checks, such as comparing different fields for consistency. For example, a form listener method might compare two dates to ensure that one precedes the other. If the end date precedes the start date, an error message can be attached to one or both fields.
If there are no errors, then the listener method advances to the next page, which shows a confirmation. A real application would save this address information to a database before continuing on, but that is beyond the scope of this example.
Validating Input based on Regular Expressions
The PatternValidator is used to validate user input against a regular expression pattern. PatternValidator is used in the Registration page to validate the zip code entered by the user, ensuring that it is either a traditional five digit zip code, or an extended nine digit zip code. It makes use of the Jarkarta ORO regular expression library (which is also used in the Tapestry framework). The source code for the PatternValidator class is shown in listing 04-xx-PatternValidator.
Listing 04-xx-PatternValidator: PatternValidator.java
package examples.register;
import net.sf.tapestry.ApplicationRuntimeException;
import net.sf.tapestry.form.IFormComponent;
import net.sf.tapestry.valid.BaseValidator;
import net.sf.tapestry.valid.ValidatorException;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.PatternCompiler;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;
public class PatternValidator extends BaseValidator
{
private String _pattern;
private Pattern _compiledPattern;
private String _errorMessage;
private Perl5Matcher _matcher;
public String toString(IFormComponent field, Object value)
{
if (value == null)
return null;
return value.toString();
}
public Object toObject(IFormComponent field, String input)
throws ValidatorException
{
if (checkRequired(field, input))
return null;
if (!match(input))
throw new ValidatorException(_errorMessage, null);
return input;
}
protected boolean match(String input)
{
if (_compiledPattern == null)
{
PatternCompiler compiler = new Perl5Compiler();
try
{
_compiledPattern = compiler.compile(_pattern);
}
catch (MalformedPatternException ex)
{
throw new ApplicationRuntimeException(ex);
}
}
if (_matcher == null)
_matcher = new Perl5Matcher();
return _matcher.matches(input, _compiledPattern);
}
public String getErrorMessage()
{
return _errorMessage;
}
public String getPattern()
{
return _pattern;
}
public void setErrorMessage(String errorMessage)
{
_errorMessage = errorMessage;
}
public void setPattern(String pattern)
{
_pattern = pattern;
_compiledPattern = null;
}
}
PatternValidator extends the framework class BaseValidator. BaseValidator is abstract, and implements the IValidator interface. It provides the boolean required property, plus a bit of support for client-side scripting (which allows the validator to generate client-side JavaScript to perform validations entirely within the client web browser).
The two key methods in a validator are toString() and toObject(). Method toString() is used to convert an object, read from the ValidField's value parameter, and convert it to a string. This method is used when the ValidField renders, the converted string is used as the value attribute of the HTML <input> element.
A validator should always be able to handle null input, though the normal course of action is to simply return null.
The meat of the validator is in the complementary toObject() method. This method is invoked when the form is submitted to convert a string, supplied by the end user, into an object. This is where all conversions and validations take place.
The first step is to invoke the method checkRequired(), which is supplied by BaseValidator. This method performs two functions: it returns true if the input is null or empty (an empty string is length zero, or contains only white space), and also throws a ValidatorException if the validator is required (and the input is null or empty). As configured in the Registration page, the zip code field is optional (not required), so if the user decides not to enter a value, the field will not be in error.
Assuming a value was supplied, the method continues by invoking method match(). If the input from the user does not match the pattern, then a ValidatorException is thrown. The ValidatorException is built around the supplied error message (another configurable property of the PatternValidator). In addition, the input provided by the user is passed along, in the exception. Thi
s exception is caught by the validation delegate, which records the error message and the invalid input for use later, when the page is rendered again.
If match() returns true, then the input value becomes the return value for the toObject() method. Ultimately, this value will be assigned to the property bound to the ValidField's value parameter.
The match() method uses the ORO API to compile the pattern and match the compiled pattern against user input. The class does a little bit of caching, since compiling a string to a Pattern is somewhat expensive; it shouldn't be done every single time. match() returns true when the input matches the pattern and false otherwise.
Customizing Label and Field Decorations
In figure 04-xx-RegistrationStart, all the required fields are marked with a red asterisk (to the right of the field). A glance at the HTML template for the page, in listing 04-xx-Registration, shows that these markers are not in the template itself. This is one example of field decoration, one of the functions of the validation delegate. Additionally, we've seen that FieldLabels and ValidFields are also decorated when they are in error.
The validation delegate has five methods that are invoked at appropriate times to handle label and field decorations. Just before and just after a FieldLabel renders, the delegate methods writeLabelPrefix() and writeLableSuffix() are invoked. The RegisterDelegate class implements these two methods as follows:
public void writeLabelPrefix(
IFormComponent component,
IMarkupWriter writer,
IRequestCycle cycle)
throws RequestCycleException
{
if (isInError(component))
{
writer.begin("span");
writer.attribute("class", "label-error");
}
}
public void writeLabelSuffix(
IFormComponent component,
IMarkupWriter writer,
IRequestCycle cycle)
throws RequestCycleException
{
if (isInError(component))
{
writer.end(); // span
}
}
The FieldLabel invokes these methods, passing the ValidField it is connected to as the component parameter. These two methods rely on the isInError() method, which returns true if the component is in error. When this is true, the delegate starts a <span> tag and writes the class attribute for it. The IMarkupWriter interface is provided by Tapestry; it is much like a PrintWriter, but with additional methods to streamline the output of elements and attributes, as shown.
The <span> tag is ended inside the writeLabelSuffix() method when it invokes end() on the writer. In between the two methods, the FieldLabel will have gotten the displayName from the ValidField and written that to output.
Just as the FieldLabel delegates part of its rendering to the validation delegate, so does the ValidField. Three metehods of the validation delegate are used to decorate fields: writePrefix(), writeSuffix() and writeAttributes(). RegisterDelegate implements the latter two methods; the default writePrefix() method does nothing.
public void writeAttributes(
IMarkupWriter writer,
IRequestCycle cycle,
IFormComponent component,
IValidator validator)
throws RequestCycleException
{
if (isInError())
writer.attribute("class", "field-error");
}
public void writeSuffix(
IMarkupWriter writer,
IRequestCycle cycle,
IFormComponent component,
IValidator validator)
throws RequestCycleException
{
if (validator.isRequired())
{
writer.printRaw(" ");
writer.begin("span");
writer.attribute("class", "required-marker");
writer.print("*");
writer.end();
}
if (isInError())
{
writer.printRaw(" ");
writer.beginEmpty("img");
writer.attribute("src", "images/field-error.png");
writer.attribute("width", 16);
writer.attribute("height", 16);
}
}
The first method, writeAttributes() is called by the ValidField after it has written the <input> tag and all of its attributes, but before it closes the tag. This gives the validation delegate a chance to add additional attributes to the tag. In this case, the delegate checks if the current component is in error; if so, a class attribute is written that, combined with the page's stylesheet, displays the offending field as white text against a red background.
The writeSuffix() method is invoked by the ValidField after it closes the <input> tag. RegisterDelegate queries the ValidField's validator (which is passed in) to see if the validator requires a value. If so, the delegate writes HTML to display the required marker.
Likewise, if the field is in error, HTML is written which marks the field using a specific image.
4.2.5 Enabling Client-Side Validation
Performing validations when the form is submitted is very powerful, but an even better solution is to not submit the form until the fields are valid within the client web browser. Accomplishing this requires client-side scripting, JavaScript event handlers that are hooked into the form submission and perform validations in the client similar to those that occur in the server.
The examples include a second version of the Registration page, Registration2. It is virtually identical to the first page, except that in the page specification, client scripting is enabled. Figure 04-xx-RegistrationClientValidation shows this version of the page in operation.
Figure 04-xx-RegistrationClientValidation: With client validation enabled, submitting the form causes validations to occur. After the user clicks the OK button, the cursor will be moved to the Last Name field.
Enabling this support is requires a small change to the page specification. The bean property clientScriptingEnabled must be set to true.
<bean name="required"
class="net.sf.tapestry.valid.StringValidator"
lifecycle="page">
<set-property name="required" expression="true"/>
<set-property name="clientScriptingEnabled" expression="true"/>
</bean>
<bean name="stateValidator"
class="net.sf.tapestry.valid.StringValidator"
lifecycle="page">
<set-property name="required" expression="true"/>
<set-property name="minimumLength" expression="2"/>
<set-property name="clientScriptingEnabled" expression="true"/>
</bean>
To support browsers where JavaScript does not exist, or is disabled, all validations still occur on the server, when the form is submitted. Some validations may simply not be possible on the client side, they may be too complex to express in JavaScript, or require access to data that isn't available in the client, such as information from a database.
Creating client side support for a validator is much more involved than creating server-side validations. Tapestry includes the necessary tools for dynamically generating the JavaScript, and the IValidator interface includes a method for this purpose, renderValidatorContribution(), but getting validations to work still requires planning, and a good understanding of both Tapestry and JavaScript.
4.2.6 Handling Form-Level Validations
So far, we've covered how to validate individual input fields. Although we can do some complex validations this way, we are at a loss when it comes to validations that involve more than one field at a time. Those kind of validations occur inside the form's listener method. Figure 04-xx-Dates shows an example of this kind of form validation, where two input date fields are checked to ensure that they form a valid range (the end date must not precede the start date).
Figure 04-xx-Dates: Although the input values are valid dates, the end date precedes the start date as is rejected with an error message.
There's no need to throw away the rest of the validation subsystem to handle these kinds of cases; it's quite possible for a form's listener method to mark fields in error, just as easily as a validator can.
Listing 04-xx: Listener Method for Dates Page
public void formSubmit(IRequestCycle cycle)
{
IValidationDelegate delegate =
(IValidationDelegate) getBeans().getBean("delegate");
if (delegate.getHasErrors())
return;
Date startDate = getStartDate();
Date endDate = getEndDate();
if (startDate.after(endDate))
{
IFormComponent inputEnd =
(IFormComponent) getComponent("inputEnd");
delegate.setFormComponent(inputEnd);
delegate.record("En
d Date must be after Start Date.",
ValidationConstraint.CONSISTENCY);
return;
}
DatesConfirm next = (DatesConfirm)cycle.getPage("DatesConfirm");
next.setStartDate(startDate);
next.setEndDate(endDate);
cycle.setPage(next);
}
This listener method starts in much the same way as the previous example; it gets the validation delegate and simply returns if the delegate already has errors. Validation errors at this point are simply formatting errors in the user input, or are the result of the user omitting one of the fields (both of which are required). The remaining code in the method is only executed if there are no fundamental input errors, in which case both the startDate and endDate properties will have been supplied by the respective ValidField components (inputStart and inputEnd).
If the two dates compare wrong, with the start date occurring after the end date, then an form-level validation error has occurred. The first step is to identify the field to be associated with the error. This isn't strictly necessary; it is entirely acceptable to not invoke the setFormComponent() method, in which case the error is recorded but will not be associated with any field within the form.
In this example, we identify the end date as the error field (we could just as easily mark the start date). The validation delegate's record() method is how we assign an error to the field. The final parameter, ValidationConstraint.CONSISTENCY is an Enum of different possible reasons the field is in error. If none of the provided constraints values are appropriate, then null is an acceptable value. CONSISTENCY is used to indicate that a cross-field consistency error occurred.
The validation constraint is not used by the default implementation of IValidationDelegate, or by our custom subclass. It is provided in speculation that a more clever validation delegate implementation may have a need for it.
In any case, once the field is marked in error, the listener method returns which will cause a redisplay of the page, complete with error messages and field decorations.
Tapestry's validation subsystem, centered around the ValidField component, is a tremendous boon to web application usability. Tapestry comes with a number of predefined validations, but is completely open-ended in terms of adding new ones. With only a small amount of coding, it is possible to precisely control the look and feel of label and field decorations.
Once again, large amounts of coding simply disappear into the framework, and what little coding remains (in terms of new validators and validation delegates) can be easily reused within the application, or even in new applications. Tapestry makes it simple to create a polished, useable user interface.
4.3 Recoding data into the form with Hidden
One of the more vexing problems in web application development is dealing with the browser back button. In a traditional GUI, the user interface displayed to the user is always in perfect synchronization with the running application, but in a web application this isn't always so. Because the user can hit the browser back button to return to a previous page, or a previous rendering of a page, and still click links and submit forms, it is all too easy for the user interface to be out of synch with the state of the application running on the server.
For example, consider a typical e-commerce application with a product catalog. A user may start a search for, say, digital cameras. They eventually reach a product details page for a particular camera, perhaps a Minolta. From there, they click on a "related items" link, and get the product details for a similar Nikon camera.
Unsatisfied with the Nikon, the user hits the browser back button, returning to the page displaying the Minolta camera. The user, these are different and distinct pages, exactly as if they were thumbing through a print catalog. To the application, these are different configurations of the same page, rendered at different times.
The user enters a quantity into a form within the product details page and clicks the "add to shopping cart" button to add the camera into their cart. Which camera gets added to the cart: the Minolta or the Nikon? It depends on how the application is coded.
The application may store the identity of the product displayed on the product details page on the server. Tapestry includes a facility for this, persistent page properties, but even a non-Tapestry application could do this, by storing an attribute into the HttpSession. If the application operates this way, the user will be surprised by a Nikon camera in their shopping cart, not the Minolta they were expecting.
What's needed is a way to encode into the Form the identity of the camera into the form itself, so that when the form is submitted the correct camera is added to the shopping cart. In traditional web application development, this is accomplished by including a hidden field in the form. A hidden field is created using the HTML element <input type="hidden">. It works like a text field, except that no user interface element is created in the web browser, the hidden field is, simply, hidden.
Tapestry solves this class of problem using the Hidden component. The Hidden component works much like a TextField component; when it renders, it reads a domain object property bound to its value parameter. The property does not have to be a string; like the service parameters of a DirectLink component, the property value is encoded along with its type.
When the form is submitted, the property is extracted from the request and written back into the domain object property. A listener method may be specified; if so the listener is invoked, which gives the page a chance to perform any additional operations.
In the e-commerce example, the Hidden component would record just the identity of the Product entity for the camera. This might be a SKU or a database primary key. The listener method, invoked after the id property is restored, can then go to the database to read the Product component and continue with the process of adding the camera to the shopping cart.
4.4 Looping within a Form using ListEdit
Another problem related to synchronization between the client and the server is dealing with lists. 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 quantities of all the items in the shopping cart to be edited in one place, but the same concept applies in many other types of applications.
As we've seen, the Form rewind process is very sensitive to the exact components that render and in which order. For simple Forms, without loops or conditionals, this is never an issue. However, for complex forms, with loops and conditionals, it is all too easy for a Form submission to get out of sequence with a Form render, resulting in a StaleLinkException. When a StaleLinkException is thrown, the user is presented with a page describing the error
never a good thing. Although Tapestry includes ways to intercept the StaleLinkException and recover from it to some degree, it is better to avoid the exception in the first place.
The easiest way for a synchronization 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. For example, if a loop works off data from a database, and the database changes (adding or removing items). Another scenario is related to the browser back button, where the user may backtrack to a page that was rendered when the application was in a very different state.
So, just as Hidden can store a single value into a form, the ListEdit component is provided for storing a list of values into a form. ListEdit has an interface that is very similar to the Foreach component; it has source and value bindings just like Foreach. When a ListEdit is rendering, it behaves exactly like Foreach component, with one difference: it records a series of hidden fiel
ds into 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.
However, when the enclosing Form is rewinding, ListEdit works very differently. It now reads the hidden fields it previously recorded; it doesn't use its source parameter at all when rewinding. It still updates the property bound to its value parameter, just like a Foreach component does, before rending its body.
In addition, a ListEdit component may be connected to a listener method. It invokes the listener method just after setting the value both when the rendering and when rewinding. Like a Hidden component's listener, the ListeEdit's listener is used to synchronize any properties based on the series of ids stored into the form.
4.5 Handling File Uploads
A common feature of many web applications is support for file uploads. Tapestry supports file uploads quite seamlessly, using the Upload component. Normally, file uploads are a very tricky proposition; when a web browser submits a request with a file upload, the normal encoding used to express the data (the MIME type XXX) is not used. Instead a different encoding, multipart/form-data, is used for the upload.
This alternate encoding allows any number of file uploads, interspersed with normal query parameter values (from the other form elements), to be sent from the client to the server in one large binary stream. This difference in encoding should not be any more of an issue to a servlet application developer than the difference between the HTTP GET and PUT requests. Alas, the Servlet API does not include the ability to parse and interpret multipart/form-data content, which makes handling uploaded files an uphill proposition.
Tapestry does include this ability, and can easily and seamlessly handled forms that contain a mix of ordinary form elements and file uploads. When a form is submitted containing an uploaded file, Tapestry extracts the file content from the request and stores it in memory, or in a temporary file (if the file content is large enough).
To the application, an uploaded file is represented by an instance of the interface IUploadFile. From this, an application can retrieve:
The name of the file (on the client).
The complete path of the file (on the client).
The MIME content type (as reported by the client).
The content of the file, as a java.io.InputStream.
The content of the file is deleted at the end of the request, so if the uploaded file is to be used later, it must be stored persistently, either to the server's file system or into a database.
Figure 04-xx-UploadPage shows a page that uses an Upload component.
Figure 04-xx-UploadPage: A page containing an Upload component. Clicking the Browse button will raise a file selection dialog.
It is just as simple to add an Upload component to a Tapestry Form as any other kind of component:
<input type="file" jwcid="@Upload" file="[[ file ]]"/>
Simply including an Upload component inside a Form automatically changes the encoding type of the Form to be multipart/form-data (in Tapestry release 2.3 and earlier this was not so). This is another example of the dynamic nature of Tapestry; you are not responsible for this background detail (selecting the form's encoding type), the components automatically do the right thing.
Once the form is submitted and the file is uploaded, the file instance can be passed to another page just like a simple value such as a string or number. Inside the Upload page, the listener method does just that:
public void formSubmit(IRequestCycle cycle)
{
IUploadFile file = getFile();
if (file == null)
return;
UploadResults next = (UploadResults) cycle.getPage("UploadResults");
next.setFile(file);
cycle.setPage(next);
}
The second page, UploadResults, displays the data available about the uploaded file, followed by a by a dump of the contents of the file (in hexadecimal and ASCII). This is shown in figure 04-xx-UploadOutput.
Figure 04-xx-UploadOutput: After the file is uploaded, the application displays the information available about the uploaded file.
Getting the output for the first three fields (name, path and content type) is completely straight-forward:
<table border="0">
<tr>
<th>Name:</th>
<td><span jwcid="@Insert" value="[[ file.fileName]]">
File Name</span></td>
</tr>
<tr>
<th>Path:</th>
<td><span jwcid="@Insert" value="[[ file.filePath ]]">
File Path</span></td>
</tr>
<tr>
<th>Content Type:</th>
<td><span jwcid="@Insert" value="[[ file.contentType ]]">
text/html</span></td>
</tr>
Getting the output for the binary output is a bit more involved. Tapestry doesn't have a built-in component for this kind of output, but it does include a utility class, BinaryDumpOutputStream. BinaryDumpOutputStream is a filter that takes as input a stream of bytes and produces as output the kind of text shown in figure 04-xx-UploadOutput. Fortunately, as we've seen before, components aren't the only things that can render in Tapestry. The UploadResults page includes an inner class that can render the contents of an IUploadFile.s
private static class ContentRenderer implements IRender #1
{
private IUploadFile _file;
ContentRenderer(IUploadFile file)
{
_file = file;
}
public void render(IMarkupWriter writer, IRequestCycle cycle) #2
throws RequestCycleException
{
try
{
StringWriter buffer = new StringWriter();
BinaryDumpOutputStream out =
new BinaryDumpOutputStream(buffer);
out.setBytesPerLine(32);
out.setShowAscii(true);
InputStream in = _file.getStream(); #3
copy(in, out);
in.close();
out.close();
writer.print(buffer.getBuffer().toString());
}
catch (IOException ex)
{
throw new RequestCycleException(
"Unable to generate binary dump.", null, ex);
}
}
private void copy(InputStream in, OutputStream out)
throws IOException
{
byte[] buffer = new byte[1000];
while (true)
{
int length = in.read(buffer);
if (length < 0)
return;
out.write(buffer, 0, length);
}
}
}
(annotation) <#1 IRender is the interface for objects that can render part of a response.>
(annotation) <#2 render() is the method defined by the IRender interface.>
(annotation) <#3 This is how to obtain the content of the uploaded file.>
To get this output, we make use of a Delegator component in the template, as we did early to display validation error messages:
<pre>
<span jwcid="@Delegator" delegate="[[ contentRenderer ]]"/>
</pre>
All that's left is to provide a contentRenderer property on the page:
public IRender getContentRenderer()
{
return new ContentRenderer(getFile());
}
4.6 Creating Pop-Up Date Selections using DatePicker
Last but not least on our tour of advanced Tapestry form components is the DatePicker component. The DatePicker, shown in figure 04-xx-DatePicker, allows users to enter dates. Unlike a ValidField combined with a DateValidator, this isn't simply a way to format and parse input. The DatePicker generates client-side JavaScript that allows a pop-up window to be displayed when the use clicks an "activate" button to the right of the text field.
Figure 04-xx-DatePicker. TheDatePicker component uses client-side JavaScript to create a popup window for selecting dates.
To demonstrate the DatePicker, we'll rewrite the Dates page to use DatePicker instead of ValidField. This has advantages and disadvantages. The DatePicker is a more highly refined user interface which is a plus. On the downside, DatePicker requires more manual work to integrate into the validation subsystem, and provides less control over the output: a validator simply isn't in the loop, since validators only work with ValidFields. Finally, a ValidField is completely functional when the client web browser does not support JavaScript (or has simply disabled it), but a DatePicker is entirely reliant on the JavaScript being present and enabled.
For the most
part, the HTML template for the revised Dates page is the same, with some minor differences around the labels and fields.
<tr>
<th><span jwcid="@FieldLabel" displayName="Start Date" #1
field="[[ components.inputStart ]]">
Start Date</span></th>
<td><input type="text" jwcid="inputStart" size="10"/>
<span class="required-marker">*</span> #2
<span jwcid="@Conditional" #3
condition="[[ beans.delegate.inError ]]">
<img src="images/field-error.png"/>
</span>
</td>
</tr>
(annotation) <#1 The label must provide the displayName, since the DatePicker does component not.>
(annotation) <#2 The template must explicitly include the required marker.>
(annotation) <#3 The template includes a check to see if the current field, the DatePicker, is in error.>
When using a ValidField, the displayName (used by the FieldLabel component) is provided as a parameter of the ValidField. Without a ValidField, we must instead set the displayName directly on the FieldLabel component. Also, work normally done by the validation delegate (decorating fields that are required or in error) must be done manually, in the template, since the decoration methods provided by the validation delegate are invoked from within the ValidField component.
In addition, even simple checks for required fields must now be done inside the form's listener method.
public void formSubmit(IRequestCycle cycle)
{
IValidationDelegate delegate =
(IValidationDelegate) getBeans().getBean("delegate");
Date startDate = getStartDate();
Date endDate = getEndDate();
if (startDate == null)
error(delegate, "inputStart", "Start Date is required.",
ValidationConstraint.REQUIRED);
if (endDate == null)
error(delegate, "inputEnd", "End Date is required.",
ValidationConstraint.REQUIRED);
if (delegate.getHasErrors())
return;
if (startDate.after(endDate))
{
error(
delegate,
"inputEnd",
"End Date must be after Start Date.",
ValidationConstraint.CONSISTENCY);
return;
}
DatesConfirm next = (DatesConfirm) cycle.getPage("DatesConfirm");
next.setStartDate(startDate);
next.setEndDate(endDate);
cycle.setPage(next);
}
protected void error(
IValidationDelegate delegate,
String componentId,
String message,
ValidationConstraint constraint)
{
IFormComponent component =
(IFormComponent) getComponent(componentId);
delegate.setFormComponent(component);
delegate.record(message, constraint);
}
4.7 Conclusion
Tapestry's more advanced form-related components are all built around usability: both for you, the developer, and your end-users. Tapestry components encapsulate not only presentation issues, but also handle conversions between domain properties and the string values used wtihin HTML. Tapestry's component based approach to form handling makes it possible to build forms with a much higher level of complexity without getting bogged down in the mundane details of string conversions and field naming. Once again, Tapestry makes it possible to get more work done, better and faster, leaving you more time to concentrate on the specifics of your application.
PAGE 42
PAGE 35
Authors Template Manning Publications Co. PAGE 42
Authors Template Manning Publications Co. PAGE 35
â
l
&
l
l
[...1579 lines suppressed...]
+\
<½
ö
od
od
íÀ@
ööáÂ@
Index: index.html
===================================================================
RCS file: /cvsroot/tapestry/TapestryBook/doc/index.html,v
retrieving revision 1.6
retrieving revision 1.7
diff -C2 -d -r1.6 -r1.7
*** index.html 2 Mar 2003 22:56:19 -0000 1.6
--- index.html 4 Mar 2003 16:43:27 -0000 1.7
***************
*** 34,56 ****
<li><a href="Book-Introduction.doc">Introduction</a> (Word)</li>
<li><a href="Chapter-01-IntroducingTapestry.doc">Chapter 01 - Introducing Tapestry</a> (Word)</li>
! <li><a href="Chapter-02-GettingStarted.doc">Chapter 02 - Getting Started</a> (Word)
! <ul>
! <li><a href="Figure-02-01-Hangman.tif">Figure 01 Hangman</a> (TIFF)</li>
! <li><a href="Figure-02-02-HangmanFlow.eps">Figure 02 HangmanFlow</a> (EPS)</li>
! <li><a href="Figure-02-03-ProjectLayout.eps">Figure 03 ProjectLayout</a> (EPS)</li>
! <li><a href="Figure-02-04-HomePage.tif">Figure 04 HomePage</a> (TIFF)</li>
! </ul></li>
! <li><a href="Chapter-03-Forms.doc">Chapter 03 - Tapestry Forms</a> (Word)
!
! <ul>
! <li><a href="Figure-03-xx-ToDo.tif">Figure XX ToDo</a> (TIFF)</li>
! <li><a href="Figure-03-xx-ToDo2.tif">Figure XX ToDo2</a> (TIFF)</li>
! <li><a href="Figure-03-xx-RegistrationStart.tif">Figure XX RegistrationStart</a> (TIFF)</li>
! <li><a href="Figure-03-xx-RegistrationError.tif">Figure XX RegistrationError</a> (TIFF)</li>
! </ul>
!
! </li>
!
! <li><a href="Chapter-04-Services.doc">Chapter 04 - Servlets, Engines and Engine Services</a> (Word)</li>
</ul>
--- 34,41 ----
<li><a href="Book-Introduction.doc">Introduction</a> (Word)</li>
<li><a href="Chapter-01-IntroducingTapestry.doc">Chapter 01 - Introducing Tapestry</a> (Word)</li>
! <li><a href="Chapter-02-GettingStarted.doc">Chapter 02 - Getting Started</a> (Word)</li>
! <li><a href="Chapter-03-Forms.doc">Chapter 03 - Tapestry Forms</a> (Word)</li>
! <li><a href="Chapter-04-AdvancedForms.doc">Chapter 04 - Advanced Forms</a> (Word)</li>
! <li><a href="Chapter-05-Services.doc">Chapter 05 - Servlets, Engines and Engine Services</a> (Word)</li>
</ul>
Index: Chapter-03-Forms.doc
===================================================================
RCS file: /cvsroot/tapestry/TapestryBook/doc/Chapter-03-Forms.doc,v
retrieving revision 1.5
retrieving revision 1.6
diff -C2 -d -r1.5 -r1.6
Binary files /tmp/cvsJi51x8 and /tmp/cvsOdxyqe differ
-------------------------------------------------------
This SF.net email is sponsored by: Etnus, makers of TotalView, The debugger
for complex code. Debugging C/C++ programs can leave you feeling lost and
disoriented. TotalView can help you find your way. Available on major UNIX
and Linux platforms. Try it free. www.etnus.com