Tapestry 3 pages were defined as an extension of one of it's built in page classes, usually BasePage. These page classes are defined as "abstract" and Tapestry takes the abstract class you define and dynamically creates a subclass all fleshed out with standard properties and functionality at runtime.
Thus a typical page might be defined as follows:
public abstract class Checkout extends BasePage implements IExternalPage, PageRenderListener {
Now in tapestry 5 it would just be a simple class:
public class Checkout {
Note that there might be interface implementations or even class extensions used, but would be for special features required by the developer, and nothing directly to do with basic page functionality.
Data Models defined in Tapestry 3 page classes would also be abstract:
public abstract CustomerData getCustomerData();
public abstract void setCustomerData(CustomerData customerData);
public abstract OrderData getOrderData();
public abstract void setOrderData(OrderData orderData);
This would allow Tapestry to create the appropriate declarations in the subclass of your abstract page.
In Tapestry 5 adding the @Property annotation would cause the appropriate getter and setter to be generated:
@Persist(PersistenceConstants.FLASH)
@Property
private ShiptoData _shiptoData;
@Persist(PersistenceConstants.FLASH)
@Property
private DetailData _detailData;
For some reason I felt like these properties needed to be retained during the redirect that happens after every form submit (hence the FLASH). I still can't seem to get my head around this, when this is required and when it is taken care of automatically.
To be clear, this FLASH persistence is sometimes required, because when Tapestry 5 processes a form submit it always causes the browser to talk to the server twice. First it posts the submit, and the page class processes the data that is submitted from the form. The only reply that is made back to the browser is a "redirect" with the URL the browser should go to. The browser responds by making a request of the server to display the page at the URL it was redirected to.
In case of an error this redirected request will often be the same page with the form so the users can fix the problem with the information and resubmit the form. But that will be blank page like any other request might be starting with. Somehow tapestry, through the session data, must know which user's form data to display because that isn't necessarily contained in the redirect url.
Thursday, May 6, 2010
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".
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".
Persist Sessions and Clients
Tapestry 5 provides the @Persist annotation. I think something like this actually showed up in T4. In T5 there are three modes:
@Persist(PersistenceConstants.SESSION)
@Persist(PersistenceConstants.CLIENTS)
@Persist(PersistenceConstants.FLASH)
SESSION is the default, so you can just specify it as:
@Persist
private String username;
As with the "Visit" in tapestry 3 the Session Persistence is supported with a cookie that acts as a key to the actual session data.
FLASH persistence is useful because of the way form submits are handled. The Page object processes a set of methods specifically for handling form submissions, then at the end of every form submission a client redirect is performed. This means that following every submit, the browser is told to request a new page (it can be the same page if there were errors on the form).
That redirected request will like involve a completely different instance of a page, even if it is the "same" page. FLASH persistence saves data for exactly one render cylce. Error messages are passed to the redirected page using FLASH persistence. Also if you want to redisplay the values that were entered into the form so that the user can make changes to clear the error instead of retyping everything, then you will have to either FLASH persist all of fields or FLASH persist a primary key which will allow you to retrieve the data.
Fortunately it looks like error messages stored using "recordError" and properties linked to Form templates (i.e. input tags) are all automatically FLASH persisted. Exactly what else is FLASH persisted is still a mystery to me.
What I do know is if you create a private property to use as a form "context" for the purpose of maintaining a "primary key" for the life of the form, and that property has a custom getter because it is a complex class that requires serialization, then you will have to define it using:
@Persist(PersistenceConstants.FLASH)
private Key _key;
If you have other one time messages or text besides the ones saved using the "record.Error" function, like maybe something that says "Changes saved" then you will need to apply the @Persist(PersistenceConstants.FLASH) annotation to those as well.
CLIENT persistance is almost useless. I guess if there was no way to keep track of a session it might be helpful. The problem with it is any data placed in CLIENT persistence must be serialized into every link and every form POST on the page. If the data is a little bit big then your page will become huge and the URLs too long. So long in fact that data might get truncated by some browsers.
@Persist(PersistenceConstants.SESSION)
@Persist(PersistenceConstants.CLIENTS)
@Persist(PersistenceConstants.FLASH)
SESSION is the default, so you can just specify it as:
@Persist
private String username;
As with the "Visit" in tapestry 3 the Session Persistence is supported with a cookie that acts as a key to the actual session data.
FLASH persistence is useful because of the way form submits are handled. The Page object processes a set of methods specifically for handling form submissions, then at the end of every form submission a client redirect is performed. This means that following every submit, the browser is told to request a new page (it can be the same page if there were errors on the form).
That redirected request will like involve a completely different instance of a page, even if it is the "same" page. FLASH persistence saves data for exactly one render cylce. Error messages are passed to the redirected page using FLASH persistence. Also if you want to redisplay the values that were entered into the form so that the user can make changes to clear the error instead of retyping everything, then you will have to either FLASH persist all of fields or FLASH persist a primary key which will allow you to retrieve the data.
Fortunately it looks like error messages stored using "recordError" and properties linked to Form templates (i.e. input tags) are all automatically FLASH persisted. Exactly what else is FLASH persisted is still a mystery to me.
What I do know is if you create a private property to use as a form "context" for the purpose of maintaining a "primary key" for the life of the form, and that property has a custom getter because it is a complex class that requires serialization, then you will have to define it using:
@Persist(PersistenceConstants.FLASH)
private Key _key;
If you have other one time messages or text besides the ones saved using the "record.Error" function, like maybe something that says "Changes saved" then you will need to apply the @Persist(PersistenceConstants.FLASH) annotation to those as well.
CLIENT persistance is almost useless. I guess if there was no way to keep track of a session it might be helpful. The problem with it is any data placed in CLIENT persistence must be serialized into every link and every form POST on the page. If the data is a little bit big then your page will become huge and the URLs too long. So long in fact that data might get truncated by some browsers.
Server State what replaces Visit object?
This one is easy. T5 includes an annotation called @SessionState.
Just declare:
@SessionState
private Visit visit;
And you can use your old visit class from Tapestry 3 without modification. You won't have to do anything like Visit visit = (Visit) getVisit(); to instantiate it. It'll just be there.
Just declare:
@SessionState
private Visit visit;
And you can use your old visit class from Tapestry 3 without modification. You won't have to do anything like Visit visit = (Visit) getVisit(); to instantiate it. It'll just be there.
Monday, April 19, 2010
Select Drop-down List. Where is PropertySelection?
Functionally SELECTS work pretty much the same, except you now you need to build the whole list for Tapestry to retrieve all at once. Under Tapestry 3 I was already building a List of Vectors in my Model for drop downs that gets used by both the Swing (desktop gui) application and the Tapestry3 web app. That meant that I was making a list, the Tapestry3 PropertySelector object was reading that list one option at a time and building it's own separate copy of the list.
This was done through an IPropertySelectionModel interface. Now instead the list goes all at once directly to Tapestry and it keeps track of items and the current selection for you.
For drop-down selects my model which previously implemented IPropertySelectionModel was converted to extend AbstractSelectModel.
So previously in our template (html) file under Tapestry 3 we would have something like this:
<select jwcid="@PropertySelection"
value="ognl:shiptoData.state"
model="ognl:stateSelectModel"/>
It only looks a little simpler for Tapestry 5:
<select t:type="Select"
value="shiptoData.state"
model="StateSelectModel"/>
The class declaration changes from implementing an interface to extending an abstract class definition:
Tapestry3:
public class StateSelectModel implements IPropertySelectionModel {
Tapestry5:
public class StateSelectModel extends AbstractSelectModel {
Additional code for Tapestry 5:
Add a private property for the entire list if you don't already have one (make sure you include the OptionModel interface in your delcaration):
private List optionModelList;
Then add the following two functions to your class:
public List<OptionModel> getOptions() {
optionModelList = new ArrayList<OptionModel>();
for (int i=1; i <= getOptionCount(); i++) {
optionModelList.add(new OptionModelImpl(getLabel(i), getValue(i)));
}
return optionModelList;
}
public List<OptionGroupModel> getOptionGroups() {
return null;
}
That's all there is to it. Your SELECT drop-down will work. Note that the get functions used in that getOptions() function are ones already implemented for the IPropertySelectionModel interface in Tapestry 3.
But even if you are doing this from scratch you should be able to easily provide the return values for the "label" and "value" which are both String type.
The getOptionGroups() function is just returning a null. This is a simple drop down. To do groups is slightly more complicated. You would have to have a loop like the one in getOptions, but then another loop inside of that which built an OptionModel list.
Now if you look closely at this simple example you'll notice that the servlet ends up doing the same extra step as in Tapestry 3. I'm making a list (you can assume that anyway), then the function is reading it one option at a time and making a copy of that list which is then offerered to Tapestry Select component through the getOptions() function.
The reason for this is there is another class that I am getting the values from and that class is the same one that is being used for the JComboBox in the swing application.
This was done through an IPropertySelectionModel interface. Now instead the list goes all at once directly to Tapestry and it keeps track of items and the current selection for you.
For drop-down selects my model which previously implemented IPropertySelectionModel was converted to extend AbstractSelectModel.
So previously in our template (html) file under Tapestry 3 we would have something like this:
<select jwcid="@PropertySelection"
value="ognl:shiptoData.state"
model="ognl:stateSelectModel"/>
It only looks a little simpler for Tapestry 5:
<select t:type="Select"
value="shiptoData.state"
model="StateSelectModel"/>
The class declaration changes from implementing an interface to extending an abstract class definition:
Tapestry3:
public class StateSelectModel implements IPropertySelectionModel {
Tapestry5:
public class StateSelectModel extends AbstractSelectModel {
Additional code for Tapestry 5:
Add a private property for the entire list if you don't already have one (make sure you include the OptionModel interface in your delcaration):
private List
Then add the following two functions to your class:
public List<OptionModel> getOptions() {
optionModelList = new ArrayList<OptionModel>();
for (int i=1; i <= getOptionCount(); i++) {
optionModelList.add(new OptionModelImpl(getLabel(i), getValue(i)));
}
return optionModelList;
}
public List<OptionGroupModel> getOptionGroups() {
return null;
}
That's all there is to it. Your SELECT drop-down will work. Note that the get functions used in that getOptions() function are ones already implemented for the IPropertySelectionModel interface in Tapestry 3.
But even if you are doing this from scratch you should be able to easily provide the return values for the "label" and "value" which are both String type.
The getOptionGroups() function is just returning a null. This is a simple drop down. To do groups is slightly more complicated. You would have to have a loop like the one in getOptions, but then another loop inside of that which built an OptionModel list.
Now if you look closely at this simple example you'll notice that the servlet ends up doing the same extra step as in Tapestry 3. I'm making a list (you can assume that anyway), then the function is reading it one option at a time and making a copy of that list which is then offerered to Tapestry Select component through the getOptions() function.
The reason for this is there is another class that I am getting the values from and that class is the same one that is being used for the JComboBox in the swing application.
Thursday, April 8, 2010
Passing paramters: activateExternalPage
Tapestry 5 doesn't seem to distinguish between a regular page and an external page. There is no BasePage class anymore and there are no special page interfaces like IExternal page.
Makes sense given the new architecture. It is simpler. Everything is easier than it used to be, it is just taking me longer than I expected to figure that out. I have a few more entries to make, but this one just came up and it's a quick one.
Every page has an "onActivate" function that you can override. Actually there are a lot of functions you can override, during each step in the process. Knowing exactly when these functions are called is essential to making your application interface work.
In Tapestry 3 you would have to declare your page something like this:
public abstract class Item extends BasePage implements IExternalPage, PageRenderListener {
Also in Tapestry 3 you'd have to implement "activateExtrenalPage" in order to
access the parsed query string:
public void activateExternalPage(Object[] params, IRequestCycle cycle) {
Then inside this function you can store values from the params array into class properties for later use. The query strings are pretty cryptic.
Now in Tapestry 5 it is just a straight up class:
public class Item {
You also need to inject your request identifier (giving you an interface to the usual HttpServletRequest data:
@Inject private Request _request;
Then add an onActivate function like this:
void onActivate() {
if (_request.getParameter("itemn") != null) {
_itemn = _request.getParameter("itemn");
}
}
Note that you can access parameters by readable string name. The query string parameters can then be in any order. Note that if the parameter is null that usually means it is not in the query string. You can do other things in this function, like plug in a default value, but keep in mind that onActivate gets called every time a page is either requested or a form submitted.
For more on these functions you can override take a look at the output on this page:
http://jumpstart.doublenegative.com.au:8080/jumpstart/examples/navigation/whatiscalledandwhen
These you implement by simply declaring a function named the same as these steps.
Also the Render phase can be broken down even more using annotations that represent the flow described in the chart on this page:
http://tapestry.apache.org/tapestry5/guide/rendering.html
The annotations available at this time are:
@SetupRender
@BeginRender
@BeforeRenderTemplate
@RenderTemplate
@BeforeRenderBody
@RenderBody
@AfterRenderBody
@AfterRenderTemplate
@AfterRender
@CleanupRender
Usage for example would look something like this:
@SetupRender void JimsToDoB4Render() {
ItemData itemData = new ItemData(_itemn);
_itemDescription = itemData.getDescription();
_itemPrice = itemData.getPrice();
}
Makes sense given the new architecture. It is simpler. Everything is easier than it used to be, it is just taking me longer than I expected to figure that out. I have a few more entries to make, but this one just came up and it's a quick one.
Every page has an "onActivate" function that you can override. Actually there are a lot of functions you can override, during each step in the process. Knowing exactly when these functions are called is essential to making your application interface work.
In Tapestry 3 you would have to declare your page something like this:
public abstract class Item extends BasePage implements IExternalPage, PageRenderListener {
Also in Tapestry 3 you'd have to implement "activateExtrenalPage" in order to
access the parsed query string:
public void activateExternalPage(Object[] params, IRequestCycle cycle) {
Then inside this function you can store values from the params array into class properties for later use. The query strings are pretty cryptic.
Now in Tapestry 5 it is just a straight up class:
public class Item {
You also need to inject your request identifier (giving you an interface to the usual HttpServletRequest data:
@Inject private Request _request;
Then add an onActivate function like this:
void onActivate() {
if (_request.getParameter("itemn") != null) {
_itemn = _request.getParameter("itemn");
}
}
Note that you can access parameters by readable string name. The query string parameters can then be in any order. Note that if the parameter is null that usually means it is not in the query string. You can do other things in this function, like plug in a default value, but keep in mind that onActivate gets called every time a page is either requested or a form submitted.
For more on these functions you can override take a look at the output on this page:
http://jumpstart.doublenegative.com.au:8080/jumpstart/examples/navigation/whatiscalledandwhen
These you implement by simply declaring a function named the same as these steps.
Also the Render phase can be broken down even more using annotations that represent the flow described in the chart on this page:
http://tapestry.apache.org/tapestry5/guide/rendering.html
The annotations available at this time are:
@SetupRender
@BeginRender
@BeforeRenderTemplate
@RenderTemplate
@BeforeRenderBody
@RenderBody
@AfterRenderBody
@AfterRenderTemplate
@AfterRender
@CleanupRender
Usage for example would look something like this:
@SetupRender void JimsToDoB4Render() {
ItemData itemData = new ItemData(_itemn);
_itemDescription = itemData.getDescription();
_itemPrice = itemData.getPrice();
}
Monday, April 5, 2010
Tapestry 5 won't find or use my AppModule.java class
This one was dumb. It took me a more time than I'll ever admit to find that I mistyped the source package name for "services". You must place AppModule.java in the services package and the package name must be the main "tapestry.app-package" name (see web.xml) plus the word "services" (e.g. test.services).
I suppose if I had used Maven to set this up, it'd been done right already.
In case you didn't find the documentation, the class "AppModule" is just the filter name (capitalized) plus the word "Module". If you setup something else for your filter in web.xml then you need to use a different name for that class.
I suppose if I had used Maven to set this up, it'd been done right already.
In case you didn't find the documentation, the class "AppModule" is just the filter name (capitalized) plus the word "Module". If you setup something else for your filter in web.xml then you need to use a different name for that class.
Subscribe to:
Posts (Atom)