SurveyPage

The SurveyPage is where survey information is collected. It initially creates a Survey instance as a persistent page property. It uses a Form component and a number of other components to edit the survey.

When the survey is complete and valid, it is added to the database and the results page is used as an acknowledgment.

The SurveyPage also demonstrates how to validate data from a TextField component[6], and how to display validation errors. If invalid data is enterred, then the user is notified (after submitting the form):

Figure 10.7. Surver Form (w/ error)

The HTML template for the page is relatively short. All the interesting stuff comes later, in the specification and the Java class.

Figure 10.8. SurveyPage.html

<jwc id="border">

<jwc id="ifError">
<table border=1>
<tr>
<td bgcolor=red>
<font style=bold color=white>
<jwc id="insertError"/>
</font> </tr> </tr> </table>
</jwc>

<jwc id="surveyForm">

<table border=0>

<tr valign=top> <th>Name</th>
	 <td colspan=3><jwc id="inputName"/></td></tr>

<tr valign=top>  <th>Age</th>
	 <td colspan=3><jwc id="inputAge"/></td></tr>

<tr valign=top> <th>Sex</th>
	  <td> <jwc id="inputSex"/>  
	  </td>

	  <th>Race</th>

      <td><jwc id="inputRace"/>
      </td> </tr>

<tr valign=top> <th>Favorite Pets</th>
	<td colspan=3>
	<jwc id="inputCats"/> Cats
<br><jwc id="inputDogs"/> Dogs
<br><jwc id="inputFerrits"/> Ferrits
<br><jwc id="inputTurnips"/> Turnips</td> </tr>
<tr>
<td></td>
<td colspan=3><input type=submit value="Submit"></td> </tr>
</table>

</jwc>
</jwc>

Most of this page is wrapped by the surveyForm component which is of type Form. The form contains two text fields (nameField and ageField), a group of radio buttons (ageSelect) a pop- up list (raceSelect), and a number of check boxes (inputCats, inputDogs, inputFerrits and inputTurnips).

Most of these components are pretty straight forward: nameField and ageField are setting String properties, and the check boxes are setting boolean properties. The two other components, raceSelect and ageSelect, are more interesting.

Both of these are of type PropertySelection they are used for setting a specific property of some object to one of a number of possible values.

The PropertySelection component has some difficult tasks: It must know what the possible values are (including the correct order). It must also know how to display the values (that is, what labels to use on the radio buttons or in the pop up). These will often not be the same value; for instance, in a database-driven application, the values may be primary keys and the labels may be attributes of database objects.

This information is provided by a model (an object that implements the interface IPropertySelectionModel), an object that exists just to provide this information to a PropertySelection component.

There's a secondary question with PropertySelection: how the component is rendered. By default, it creates a pop-up list, but this can be changed by providing an alternate renderer (using the component's renderer parameter). A rendered is an object that generates HTML from the component and its model. In our case, we used a secondary, radio-button renderer.

Applications can also create their own renderers, if they need to do something special with fonts, styles or images.

All of these concepts come together in the SurveyPage specification:

Figure 10.9. SurveyPage.jwc

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE specification PUBLIC
  "-//Howard Ship//Tapestry Specification 1.1//EN"
  "http://tapestry.sf.net/dtd/Tapestry_1_1.dtd">

<specification class="tutorial.survey.SurveyPage">

  <component id="border" type="Border">
    <static-binding name="title">Survey</static-binding>
    <binding name="pages" property-path="engine.pageNames"/>
  </component>
  
  <component id="ifError" type="Conditional">
    <binding name="condition" property-path="error"/>
  </component>
  
  <component id="insertError" type="Insert">
    <binding name="value" property-path="error"/>
  </component>
  
  <component id="surveyForm" type="Form">
    <binding name="listener" property-path="formListener"/>
  </component>
  
  <component id="inputName" type="TextField">
    <static-binding name="displayWidth">30</static-binding>
    <static-binding name="maximumLength">100</static-binding>
    <binding name="text" property-path="survey.name"/>
  </component>
  
  <component id="inputAge" type="TextField">
    <static-binding name="displayWidth">4</static-binding>
    <static-binding name="maximumLength">4</static-binding>
    <binding name="text" property-path="age"/>
  </component>
  
  <component id="inputSex" type="PropertySelection">
    <binding name="value" property-path="survey.sex"/>
    <binding name="model" property-path="sexModel"/>
    <binding name="renderer" property-path="components.inputSex.defaultRadioRenderer"/>
  </component>
  
  <component id="inputRace" type="PropertySelection">
    <binding name="value" property-path="survey.race"/>
    <binding name="model" property-path="raceModel"/>
  </component>
  
  <component id="inputCats" type="Checkbox">
    <binding name="selected" property-path="survey.likesCats"/>
  </component>
  
  <component id="inputDogs" type="Checkbox">
    <binding name="selected" property-path="survey.likesDogs"/>
  </component>
  
  <component id="inputFerrits" type="Checkbox">
    <binding name="selected" property-path="survey.likesFerrits"/>
  </component>
  
  <component id="inputTurnips" type="Checkbox">
    <binding name="selected" property-path="survey.likesTurnips"/>
  </component>
  
</specification>

Several of the components, such as inputName and inputTurnips, modify properties of the Survey directly. The SurveyPage class has a survey property, which allows for property paths like survey.name and survey.likesTurnips.

The age field is more complicated, since it must be converted from a String to an int before being assigned to the survey's age property ... and the page must check that the user enterred a valid number as well.

Finally, the SurveyPage class shows how all the details fit together:

Figure 10.10. SurveyPage.java

package tutorial.survey;

import com.primix.tapestry.*;
import com.primix.tapestry.components.html.form.*;
import java.util.*;

public class SurveyPage extends BasePage
{
  private Survey survey;
  private String error;
  private String age;
  private IPropertySelectionModel sexModel;
  private IPropertySelectionModel raceModel;
  
  public IPropertySelectionModel getRaceModel()
  {
    if (raceModel == null)
      raceModel = new EnumPropertySelectionModel(
        new Race[] 
        {
          Race.CAUCASIAN, Race.AFRICAN, Race.ASIAN, 
Race.INUIT, Race.MARTIAN
        },  getBundle("tutorial.survey.SurveyStrings"), 
"Race");
        
    return raceModel;
  }
    
  public IPropertySelectionModel getSexModel()
  {
    if (sexModel == null)
      sexModel = new EnumPropertySelectionModel(
        new Sex[] 
        {
          Sex.MALE, Sex.FEMALE, Sex.TRANSGENDER, 
Sex.ASEXUAL  
        },  getBundle("tutorial.survey.SurveyStrings"), 
"Sex");
        
    return sexModel;
  }  
  
    private ResourceBundle getBundle(String resourceName)
    {
        return ResourceBundle.getBundle(resourceName, getLocale());
    }
    
  public IActionListener getFormListener()
  {
    return new IActionListener()
    {
      public void actionTriggered(IComponent component, 
IRequestCycle cycle)
      {
        try
        {
          survey.setAge(Integer.parseInt(age));
          
          survey.validate();
        }
        catch (NumberFormatException e)
        {
          // NumberFormatException doesn't provide 
any useful data
          
          setError("Value entered for age is not a 
number.");
          return;
        }
        catch (Exception e)
        {
          setError(e.getMessage());
          return;
        }
        
        // Survey is OK, add it to the database.
        
      
  ((SurveyApplication)getApplication()).getDatabase().addSurvey(survey
);
        
        setSurvey(null);  
        
        // Jump to the results page to show the totals.
        
        cycle.setPage("Results");
      }  
    };
  }
    
  public Survey getSurvey()
  {
    if (survey == null)
      setSurvey(new Survey());
        
    return survey;
  }
  
  public void setSurvey(Survey value)
  {
    survey = value;
    fireObservedChange("survey", survey);
  }
  
  public void detach()
  {
    super.detach();
    
    survey = null;
    error = null;
    age = null;
    
    // We keep the models, since they are stateless
  }
  
  public void setError(String value)
  {
    error = value;
  }
  
  public String getError()
  {
    return error;
  }
  
  public String getAge()
  {
    int ageValue;
    
    if (age == null)
    {
      ageValue = getSurvey().getAge();
      
      if (ageValue == 0)
        age = "";
      else 
        age = Integer.toString(ageValue);
    }  
    
    return age;  
  }
  
  public void setAge(String value)
  {
    age = value;
  }
}

A few notes. First, the raceModel and sexModel properties are created on-the-fly as needed. The EnumPropertySelectionModel is a provided class that simplifies using a PropertySelection component to set an Enum-typed property. We provide the list of possible values, and the information needed to extract the corresponding labels from a properties file.

Only survey is a persistent page property. The error property is transient (it is set to null at the end of the request cycle). The error property doesn't need to be persistent ... it is generated during a request cycle and is not used on a subsequent request cycle (because the survey will be re- validated).

Likewise, the age property isn't page persistent. If an invalid value is submitted, then its value will come up from the HttpServletRequest parameter and be plugged into the age property of the page. If validation of the survey fails, then the SurveyPage will be used to render the HTML response, and the invalid age value will still be there.

In the detach() method, the survey, error and age properties are properly cleared. The raceModel and ageModel properties are not ... they are stateless and leaving them in place saves the trouble of creating identical objects later.



[6] Since this tutorial was written, a suite of more powerful components for validating input have been added to the Tapestry framework.