Persistent Page State

The Tapestry framework is responsible for tracking changes to page state during the request cycle, and storing that state between request cycles. Ultimately, this is the responsiblility of the application engine. This is accomplished through page recorder objects. As a page's persistent state changes, it notifies its page recorder, providing the name of the property and the new value.

This information is stored persistently between request cycles. In a later request cycle, the page recorder combines this information with a page instance to rollback the state of the page.

Pages are blind as to how their state is stored. The basic implementation of Tapestry simply stores the page state information in memory (and serializes it with the engine, in the HttpSession), but future options may include storing the data in flat files, relational databases or even as cookies in the client browser.

Some minor burden is placed on the developer to support persistent state. The mutator method of every persistent property must include a line of code that notifies the observer of the change.

For example, consider a page that has a persistent property for storing an email address. It would implement the normal accessor and mutator methods:

private String emailAddress;

public String getEmailAddress()
{
  return emailAddress;
}

public void setEmailAddress(String value)
{
  emailAddress = value;

  fireObservedChange("emailAddress", value);
}

The mutator method does slightly more than change the private instance variable; it must also notify the observer of the change, by invoking the method fireObservedChange(), which is implemented by the class AbstractComponent. This method is overloaded; implementations are provided for every type of scalar value, and for java.lang.Object.

The value itself must be serializable (scalar values are converted to wrapper classes, which are serializable).

The page designer must provide some additional code to manage the lifecycle of the page and its persistent properties. This is necessary to support the "shell game" that allows a page instance to be separate from its persistent state, and is best explained by example. Let's pretend that the user can select a personal preference for the color scheme of a page. The default color is blue.

The first user, Suzanne, reaches the page first. Disliking the blue color scheme, she uses a form on the page to select a green color scheme. The instance variable of the page is changed to green, and the page recorder inside Suzanne's session records that the persistent value for the color property is green.

When Suzanne revisits the page, an arbitrary instance of the page is taken from the pool. The page recorder changes the color of the page to green and Suzanne sees a green page.

However, if Nancy visits the same page for the first time, what is the color? Her page recorder will not note any particular selection for the page color property. She'll get whatever was left in the page's instance variable ... green if she gets the instance last used to display the page for Suzanne, or some other color if some other user recently hit the same page.

This may seem relatively minor when the persistent page state is just the background color. However, in a real application the persistent page state information may include user login information, credit card data, the contents of a shopping cart or whatever. The way to deal with this properly is for each page with persistent state to override the method detach(). The implementation should reset any instance variables on the page to their initial (freshly allocated) values.

In our example, when Suzanne is done with the page, its detach() method will reset the page color property back to blue before releasing it into the pool. When Nancy hits the page for the first time, the page retrieved from the pool with have the expected blue property.

In other words, it is the responsibility of the developer to ensure that, as a page is returned to the pool, its state is exactly the same as a freshly created page.

In our earlier email address example, the following additional code must be implemented by the page:

public void detach()
{
  emailAddress = null;

  super.detach();
}

All properties, dynamic, transient and persistent, should be reset inside the detach() method.

Individual components on a page may also have dynamic, transient or persistent properties. If so, they should implement the PageDetachListener interface and implement the pageDetached() method and clear out such properties, just as a page does in detach().