Friday, April 23, 2010

Primary Keys on Forms and IActionListener and no more Rewinds

In Tapestry 3 you could specify a hidden field that would trigger an
event when the form was submitted:
<span jwcid="@Hidden" value="ognl:itemFormKey" listener="ognl:listeners.formKeyListener"/>

This gets fired at the beginning of the form rewind cycle so that any updating or validating that requires the primary key of the object(s) being added or updated by the form.

In Tapestry 5 the "Hidden" component that won't automatically serialize the the object like in T3 and it doesn't fire a listener. Instead you can specify a "ValueEncoder" which can be an interface added to your page or component class. This can be used to encode (toClient) and decode (toValue) objects so long as you declare the object Serializable and use some sort of method to convert it to a string (e.g. toString() or convert use base64 if it is a complex object. You can also use the toValue(String) function to instantiate any objects or lookup any data related to the primary key before the rest of the form submit is processed. The hidden field ValueEncoders are processed before the form submit processing starts copying the form fields into your page or component class. Note that I am not sure, but this order of processing might require that the Hidden component is placed at the top of the form.

-OR-


You don't have to use hidden components to accomplish the same thing.
A more direct route, which is the one I used, is to define a context for the form in the template for the page or component.

<form t:type="form" t:id="itemForm" t:context="itemFormKey" action="POST">

This automatically creates a hidden field for the itemFormKey. You will need to define an "onPrepareForSubmit" function in your page class that uses "itemFormKey" as a parameter. Note that the object _itemFormKey will need to be instantiated at least the first time the page containing the form is requested.

void onPrepareForSubmit(String itemFormKey) {

try {
_itemFormKey = (ItemFormKey) Base64.decodeToObject(itemFormKey);
}
catch(Throwable e) {
throw new RuntimeException(e);
}

}

The Base64 class is one from Robert Harder at http://iharder.net/base64

Add a getter for it so that you can encode the object (must be serializable) to a base64 string for the hidden field during rendering of the template containing the form:

public String getItemFormKey() {
try {
return Base64.encodeObject(_itemFormKey);
}
catch(Throwable e) {
return "";
}
}

You know how Tapestry 5 does no rewind cycle? What it does when a submit takes place is the page object processes a series of methods related to handling the form data. See this page for a list of the methods called in what order:
http://jumpstart.doublenegative.com.au:8080/jumpstart/examples/navigation/whatiscalledandwhen

After all this processing of the Submit is done, that's it. The tapestry page finishes by sending a URL to the browser as a page redirect. So every form submit is followed by a separate request automatically from the client (not counting Ajax). Generally you have the flexibility to redirect anywhere you want after or during processing the form submit, but you needn't do much of anything for it to redirect to the current page which is what you probably want if there is an error found during form validation in onValidateForm().

Now think about this. Since this is a redirect, you start over with a brand new instance (or a clean instance from the pool) of the page object. The "key" field (in this case itemFormKey) will be null.

In order to fix this you will need to utilize a special kind of data "persistance" available in Tapestry5 for just this purpose. It is called "FLASH" and it has nothing to do with macromedia but if you apply the annotation to the declaration in your java class, the object will be saved in the session for the duration of one more rendering. So during the "redirected" request that always happens after every form submission, that object will be available. When that one single rendering is done, the object is deleted from the session. To accomplish this all you need to do is this:

@Persist(PersistenceConstants.FLASH)
private ItemFormKey _itemFormKey;

Now the ItemFormKey will be available for as long as the current form is being used, and Submitted. If the user navigates away from the form the value will no longer be persisted, unless of course the navigation is done as an "Open in New Window".

No comments:

Post a Comment