What are pages?

Tapestry is a component based framework. Examples of this come with the framework such as TextField which will render and keep the values of a user text entry field and a more complex ones such as the DatePicker that displays a text entry with a popup Javascript calendar. Components can include as many other components as you like, each having their own layout and Java code. Everything in Tapestry is said to be a component so where do pages fit in? If you think about it for a moment pages are just special components...simple as that.

What is the minimum a page needs to work?

  1. A template. This is the View part of our application, what the page looks like and what the user can do with it There is no logic or processing going on here, that is reserved for...
  2. A Page Object. This is a Java object that is the backing for the template. Here you can retrieve and populate values that will appear in the template, clicked links from the template can call methods here and form data can be posted.
  3. An optional page specification since Tapestry 4 you don't always need this. This is an XML descriptor that glued the template to the page. I won't go into this in too much depth as I try and avoid using them just to cut down on the number of files and maintenance.

Project layout and folder structure

Templates are in the Web project under src\main\webapp

Page objects are in the Java project that backs the web project under src\main\java\PACKAGE_NAME

Page Lifecycle Overview

As the cost of creating a page is high they are lazily created and placed in a pool of pages for faster subsequent use.

The Tapestry engine will call certain events (methods) on your page during its life cycle as shown here in this psuedo-example of an Edit page that gets some data from the persistance layer displays it to the user, lets the user change values and submit the results back to the database.

Displaying Data

First we'll take a look at a the CrudList page that will display values from a Java ArrayList.

  <body jwcid="border" title="CRUD List">
    <span jwcid="@CrudResults"></span>
  </body>

Thats it? But how does it display anything with 2 lines?

This is a demonstration of the power of components. This template uses 2 custom components, "border" and "CrudResults". Tapestry components are declared within HTML by the special attribute "jwcid" (Java Web Component Id) You can use a named component who's use has been declared elsewhere in the case of the border. The "title" attribute is used by the Border component to set the display title of the page. The border component was declared in the Java page (actually the Base Page):

    @Component(type = "Border")
    public abstract Border getBorder();

The CrudResults component is anonymous, we don't give it a name, Tapestry will for us. You usually would only name a component when you need to refer to it again. The syntax with the '@' character will declare the use of a component. You can combine the 2 methods to set a named component inline such as "myCrudList@CrudResults".

The use in this case may seem a little inconsistent but it acheives 2 things. This is the only page that uses the CrudResults component so it can be anonymous whereas the border is declared in the Base Page so all site pages can use it (thus giving us a nice consistent look and feel); and it demonstrates the component declaration techniques :)

The border component gives us a consistent, write-once use-many look and feel to all pages that use it...too easy. For this site we have a header, footer, left-side nav bar and a main content part where the results will be listed. So all our pages care about is filling up the content area and this is where the CrudResults component comes in.

The CrudResults component really is just a big block of HTML with Tapestry component uses, nothing too special (or necessarily well designed for that matter, it could be broken down into sub-components itself).

Looking deeper we see things in the template such as:

   <a jwcid="@DirectLink" listener="listener:firstPage"
       parameters="ognl:visitObject.crudMetaInfo" disabled="ognl:firstPage"
       class="buttonHover"> &lt;&lt; First Page </a>

Tapestry will render a HTML hyperlink for us with all the parameters encoded and when it is clicked it will call a method called "firstPage" in our Java page. The "ognl" stands for Object Graph Notation Language which is a way of directly access objects from the Java page that sits behind the template. So in a way the template and page are glued together, each can access each others objects. The call "visitObject.crudMetaInfo" is equivalent in Java code to "getVisitObject().getCrudMetaInfo()".

More on OGNL can be found in the OGNL language guide at http://www.ognl.org/

The listener and parameter match up directly to a method in our Java page that will be called when the user clicks on the link (if you don't match them up Tapestry will let you know all about it). The method in this case looks like:

    public IPage firstPage(CrudMetaInfo crudMetaInfo) {
        this.setCurrentPage(0);
        log.info("Moving to FIRST page...");
        return getPage();
    }

To matchup the method name must equal the name given in the "listener:" and the parameter type must equal the "parameters:" type. Yes you can provide multiple parameters to the component (I think comma separated). The other interesting thing here is that the method returns a Tapestry type of IPage. If you return a page object Tapestry will display that page to the user, you can call setters on the page first and Tapestry will encode them all behind the scenes and pass them along; nice, safe and easy.

There are many more components that work in a similar way included in the Tapestry framework, this also gives way to other people writing and packaging components for others to use (see the Contrib library). All are documented here: http://jakarta.apache.org/tapestry/tapestry/index.html under Framework - Components and Contrib Library - Components.

Displaying the Results

The Java page must populate a list and then the template will use the "For" component to iterate over the list.

  public abstract class CrudResults extends CrudBaseComponent implements PageBeginRenderListener {
     .....
    // Holds the results that the template will iterate over
    public abstract List getResults();

    public abstract void setResults(List results);

    // At each iteration this param will hold the current value from the above list
    public abstract Object getCurrentRow();

    public abstract void setCurrentRow(Object obj);

    // Page event triggered by the Tapestry engine
    // This is a good place to populate our results to iterate over
    public void pageBeginRender(PageEvent event) {
       .......

        if (getResults() == null) {
            // Grab results via Cayenne
            SelectQuery query = new SelectQuery(getVisitObject().getCrudMetaInfo().getJavaClass());
            query.setPageSize(getVisitObject().getCrudMetaInfo().getPageSize());
            setResults(getDataContext().performQuery(query));
        }
       .......
    }

Beyond the embedded comments this needs some explaination. Pages and components and their parameters (the abstract getters and setters) are best declared abstract. This way Tapestry can manage the object properly; really this is because pages are pooled for efficiency and the property values must be correctly reset otherwise one user might get a pooled page that had values left over from the last user (when the page was returned to the pool). Tapestry will create the actual subclass implementation at runtime.

The template iterates over the results:

  .......
  <span jwcid="@For" source="ognl:results" value="ognl:currentRow" index="ognl:rowIndex">

     <!-- Simplification of the actual code  -->
     <span jwcid="@Insert" value="ognl:currentRow"/>
  .......

Notice again how each of the "ognl:" relates to a "getter" on the related Java page. Eg: "ognl:results" links to "public abstract List getResults()". Simple eh?

The value of the currentRow is printed out with the @Insert component. Around this we can put any kind of HTML to make it nice and pretty.

Submitting data

Take a look at another cut-down code segment:

  <form jwcid="@Form" cancel="listener:cancelChanges" method="POST">
      <table>
          <tr jwcid="@Any" class="ognl:beans.rowClass.next" element="tr">
                  <td>
                      <span jwcid="@Insert" value="ognl:currentCrudAttribute.displayName"></span>
                  </td>
                  <td>
                      <span jwcid="@Insert" value="ognl:currentCrudAttribute.value"></span>
                      <span jwcid="@RadioGroup" selected="ognl:currentCrudAttribute.value">
                          <input jwcid="@Radio" type="radio" value="ognl:true"/>True/Yes
                          <input jwcid="@Radio" type="radio" value="ognl:false"/>False/No
                      </span>
                  </td>
                  <td>
                      <span jwcid="@DatePicker" value="ognl:currentCrudAttribute.value"/>
                  </td>
                  <td>
                      <span jwcid="@TextField" value="ognl:currentCrudAttribute.value" translator="translator:number"/>
                  </td>
                  <td>
                  <span jwcid="@TextField" value="ognl:currentCrudAttribute.value"
                        translator="translator:number,pattern=#.#"/>
                  </td>
                  <td>
                      <span jwcid="@TextField" value="ognl:currentCrudAttribute.value"/>
                  </td>
          </tr>
          <tr><td><input jwcid="@Submit" type="submit" value="Save Changes"
                         action="listener:saveChanges" parameters="ognl:crudObject"/></td>

Just like with HTML to submit values in Tapestry you need a form, and wrapped inside the form you can have all types of fields such as text, radio buttons and check boxes. Tapestry takes this a step further and allows you to write your own and even comes bundled with a few such as the DatePicker. All the form fields once again map back to the Java page using OGNL. Then when the form is submitted the values are automatically wired up to the Java page parameters. In this example they all point to the same parameter, in the real code each component above is wrapped with @contrib:Choose and @contrib:When components that decide based on the attribute value which component type to display. So if the attribute type from the database/Cayenne is a Date then a DatePicker is displayed.

  public abstract class CrudAddModify extends CrudBasePage {
        .......
        public abstract CrudAttribute getCurrentCrudAttribute();

        public abstract void setCurrentCrudAttribute(CrudAttribute crudObjectBeanAttribute);

        @Bean
        public abstract EvenOdd getRowClass();

        .......

Form submit event

This is the same as when the DirectLink was clicked with the addition that all the form fields are automatically wired up to the corresponding Java page parameters.

  <input jwcid="@Submit" type="submit" value="Save Changes"
                         action="listener:saveChanges" parameters="ognl:crudObject"/>

upon clicking the submit button will call the corresponding Java page method:

   public IPage saveChanges(CrudObjectBean crudObjectBean) {
      .......
      // Save the Cayenne object etc...

      // Return to the list page
      getCrudListPage().setMsg("Changes saved...");
      return getCrudListPage();
      .......
   }

I'll explain the mechanism for commiting data object changes later. For now I'll explain again how to return to another page:

  1. Get the page that was "injected" and set any values you want to pass to it. In this case it is a simple message that the page was saved.
  2. Return the page

    To inject the page...easy:

         @InjectPage(value = "CrudList")
         public abstract CrudList getCrudListPage();