In this tutorial, we'll cover the basics of Tapestry forms, and gain an understanding of the lifecycle of a Tapestry application. We'll also see how to transfer information from one page to another.
The theme of this tutorial is an application used to track third-party libraries for Tapestry. Each project will have a number of properties:
<html jwcid="@Shell" title="Tapestry Component Database"> <body> <h1>Tapestry Component Database</h1> <p> Options: </p> <ul> <li><a jwcid="@PageLink" page="AddProject">Add New Project</a></li> </ul> </body> </html>
package tutorial.forms.data; import java.util.Date; /** * Contains the name and description of a release of a project. * * @author Howard M. Lewis Ship */ public class ProjectRelease { private String _name; private String _releaseId; private String _shortDescription; private String _longDescription; private String _category; private String _tapestryVersion; private Date _releaseDate; private boolean _public; /** * A user-specified category, used to group similar projects. */ public String getCategory() { return _category; } public void setCategory(String category) { _category = category; } /** * A longer description used on a detail page. */ public String getLongDescription() { return _longDescription; } public void setLongDescription(String longDescription) { _longDescription = longDescription; } /** * The name of the project. */ public String getName() { return _name; } public void setName(String name) { _name = name; } /** * If true, the project is visible to other users. If false, then the project is not visible. * This is used as a "draft" mode, when information about the project is not complete. */ public boolean isPublic() { return _public; } public void setPublic(boolean public1) { _public = public1; } /** * The date when the project was released. Used to generate a chronological listing. */ public Date getReleaseDate() { return _releaseDate; } public void setReleaseDate(Date releaseDate) { _releaseDate = releaseDate; } /** * The version number of the project that was released. */ public String getReleaseId() { return _releaseId; } public void setReleaseId(String releaseId) { _releaseId = releaseId; } /** * A single-line description used in an overview listing. */ public String getShortDescription() { return _shortDescription; } public void setShortDescription(String shortDescription) { _shortDescription = shortDescription; } /** * The version of Tapestry required for the project. */ public String getTapestryVersion() { return _tapestryVersion; } public void setTapestryVersion(String tapestryVersion) { _tapestryVersion = tapestryVersion; } }
<html jwcid="@Shell" title="Add New Project"> <body jwcid="@Body"> <h1>Add New Project</h1> <form jwcid="form@Form" success="listener:doSubmit"> <table> <tr> <th>Name</th> <td> <input jwcid="name@TextField" value="ognl:project.name" size="40"/> </td> </tr> <tr> <th>Release ID</th> <td> <input jwcid="release@TextField" value="ognl:project.releaseId" size="20"/> </td> </tr> <tr> <th>Short Description</th> <td> <input jwcid="short@TextField" value="ognl:project.shortDescription" size="40"/> </td> </tr> <tr> <th>Long Description</th> <td> <textarea jwcid="long@TextArea" value="ognl:project.longDescription" rows="10" cols="40"/> </td> </tr> <tr> <th>Tapestry Version</th> <td> <input jwcid="tapestryVersion@TextField" value="ognl:project.tapestryVersion" size="20"/> </td> </tr> <tr> <th>Release Date</th> <td> <input jwcid="releaseDate@DatePicker" value="ognl:project.releaseDate"/> </td> </tr> <tr> <th>Public</th> <td> <input jwcid="public@Checkbox" value="ognl:project.public"/> </td> </tr> </table> <input type="submit" value="Add Project"/> </form> </body> </html>
This template introduces a number of new components:
package tutorial.forms.pages; import org.apache.tapestry.html.BasePage; import tutorial.forms.data.ProjectRelease; /** * Java class for the AddProject page; contains a form used to collect data for creating a new * {@link tutorial.forms.data.ProjectRelease}. * * @author Howard M. Lewis Ship */ public abstract class AddProject extends BasePage { public abstract ProjectRelease getProject(); public void doSubmit() { } }
Maybe this is too minimal; if we launch the application and choose the Add New Project link, we get an exception:
The root of this exception is a null value: we defined a place to store a ProjectRelease object but didn't actually provide an instance. OGNL attempted to dereference through the null value and threw the OgnlException. Here we can see the advantage of Tapestry's exception reporting ... showing the stack of exceptions gave us context into our application (the line in the template associated with the error) without obscuring the underlying cause.
What we need to do is create an instance of ProjectRelease and store it into the property so that the TextField components can edit it. We have to be careful because in a live application, pages will be pooled and reused constantly.
For this situation, the right approach is to listen for the PageBeginRender event, and store the new instance into the property then. The ProjectRelease object will be used for the duration of the request, then discarded at the end of the request.
Listening for these lifecycle events is simple; you just need to select the correct listener interface and implement it; Tapestry will automatically register your page to receive the notifications. Here, the interface is PageBeginRenderListener :
package tutorial.forms.pages; import java.util.Date; import org.apache.tapestry.event.PageBeginRenderListener; import org.apache.tapestry.event.PageEvent; import org.apache.tapestry.html.BasePage; import tutorial.forms.data.ProjectRelease; /** * Java class for the AddProject page; contains a form used to collect data for creating a new * {@link tutorial.forms.data.ProjectRelease}. * * @author Howard M. Lewis Ship */ public abstract class AddProject extends BasePage implements PageBeginRenderListener { public abstract ProjectRelease getProject(); public abstract void setProject(ProjectRelease project); public void pageBeginRender(PageEvent event) { ProjectRelease project = new ProjectRelease(); project.setReleaseDate(new Date()); setProject(project); } public void doSubmit() { } }
The pageBeginRender() method will be invoked whenever the page renders. It is also, due to a useful quirk of Tapestry, invoked when a form within the page is submitted. Not only can we create an instance, but we have the opportunity to set some initial values for fields.
With this in place, the page will now render, and we can begin entering some data into it:
As implemented above, submitting the form doesn't appear to do anything. Sure, the form submits, and information is pulled out of the request and applied to the properties of the ProjectRelease object, but then the AddProject page is re-rendered.
So ... how did the ProjectRelease object stick around? Shouldn't we have gotten a NullPointerException again, when the form submitted and the TextField component updated property project.name? What actually happened is that the ProjectRelease object was discarded ... but when a form is submitted on a page, the PageBeginRender interface is triggered again, just like for a form render. This means that the existing code does create a new ProjectRelease instance, giving the TextField components a place to store the values from the form.
As wonderful as it is that the new ProjectRelease object gets updated, what we really want is for something to happen . We're going to change things so that a different page is displayed when the form is submitted. Further, that form will show the same information collected by the AddProject page.
To accomplish this, we're going to change the doSubmit() method, and have it obtain the ShowProject page. The easiest way to make different pages work together is by injecting one page into another. This can be done using an annotation 1 :
@InjectPage("ShowProject") public abstract ShowProject getShowProject();
This code, part of AddProject.java, establishes the connection from the AddProject page to the ShowProject page.
We can leverage that code inside doSubmit() to pass information from the AddProject page to the ShowProject page; we can also activate the ShowProject page, so that it renders the response.
public IPage doSubmit() { ShowProject showProject = getShowProject(); showProject.setProject(getProject()); return showProject; }
A lot is going on here in a very small amount of code. First off, this method is no longer void, it returns a page ( IPage is the interface all page classes must implement). When a listener method returns a page, that page becomes the active page , the page which will render the response to the client.
This is an example of what we mean when we talk about objects, methods and properties. We don't talk about the "ShowProject.html" template ... we talk about the ShowProject page, and leave the details about where its template is and what's on that template to the ShowProject page. Further, to pass information from the AddProject page to the ShowProject page, we don't muck about with HttpServletRequest attributes ... we store an object into a property of the ShowProject page.
With all this in place, we can now submit the form and see the ShowProject page:
More coming soon ...
1 Of course, annotations required JDK 1.5, which you'll need for these tutorials. If you can't use JDK 1.5 in production, that's OK. Tapestry is compatible with JDK 1.3 and has another approach, based on XML, for injecting pages or doing just about anything else that shows up as an annotation in the tutorials.