%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/fenix-renderers.tld" prefix="fr" %>
ViewState
When you use the edit
tag you are always editing an object.
So you must wrap all the pieces of information in a bean and then edit
that bean.
Suppose you, for some strange reason, need the user to introduce two ages, a date, and a gender to search for persons. You could create the following bean:
public class SearchBean implements Serializable { private int minAge; private int maxAge; private Date date; private Gender gender; public int getMinAge() { return this.minAge; } public void setMinAge(int minAge) { this.minAge = minAge; } public int getMaxAge() { return this.maxAge; } public void setMaxAge(int maxAge) { this.maxAge = maxAge; } public Date getDate() { return this.date; } public void setDate(Date date) { this.date = date; } public Gender getGender() { return this.gender; } public void setGender(Gender gender) { this.gender = gender; } }
And in some action you create the bean and put it in a request attribute.
SearchBean bean = new SearchBean(); bean.setDate(new Date()); request.setAttribute("bean", bean);
But how do we send the input to the action that will search the persons? For that
you use the action
attribute of the edit
tag. This attribute
makes the form submit the content to the specified action. By default the form is
submited to the input location, that is, the url originally requested.
<fr:edit name="bean" action="/renderers/searchPersons.do?method=search"/>
Now we need to know how, in the target action, we get access to the bean. Between interactions with the user the view state is mantained. Is through that view state the we can get the object that was edited. So in the action we would do something like:
ViewState viewState = (ViewState) RenderUtils.getViewState(); SearchBean bean = (SearchBean) viewState.getMetaObject().getObject(); // search person (...)
All we need now is the
So you need to pass some extra hidden fields to the action, use some properties from a Struts action form, or even put the form submission buttons in other place and with other names? Can you still do this?
Yes!
What you need to do is surround the renderer tag with an html:form
tag. When you do
this no form will be generated by the renderer. This means that the Submit and Cancel buttons
you are used to see next to the renderer input presentation will not be present and you
have to manually add new buttons. This gives you more control about the form and, in fact, is
the only way of changing the name of the submit button for just one page and to introduce
hidden fields for actions.
If you don't use the html:form
but use a plain form instead then you have to manually
indicate that the renderization is nested in another form. For this you can use the nested
attribute of the edit
or create
tag. If you specify nested=true
then not form will be generating automatically no mather were the fr:edit
or fr:create
is beeing used.
<html:form action="/renderers/searchPersons.do?method=search"> <html:hidden property="theProperty" value="theValue"/> <fr:edit name="bean" action="/renderers/searchPersons.do?method=search"/> <html:submit value="Save"/> <html:cancel value="Forget"/> </html:form>
The outter form now defines the target action but if some controler chooses to send the flow to other destination
they will still override the destination specified in the form. The behaviour of the "Cancel" button that is
normally generated when the fr:edit
and fr:create
are used can be reintroduced with
the html:cancel
tag. Renderers recognize Struts semantic for that button and also cancel the
viewstate processing.
Now imagine that you want to do something to the submited data before the action is executed or even choose the destination according to the user input. How can you do that? For this type of control you need to use controllers.
Controllers can be associated to form components and are executed just after the components are updated with the submited values. Each controller has access to the corresponding controlled component and to the view state. This way they can influence the global structure or aspect of what is presented, control the lifecycle of the components, redirect to another location, etc.
Controllers can only be associated to components by renderers and not from the configuration like validators. So if you need to include controllers you probably need to create a new renderer. Here is an example of a controller that redirects to different locations according to gender:
class ChooseFromGenderController extends HtmlController { @Override public void execute(IViewState viewState) { HtmlMenu genderMenu = (HtmlMenu) getControlledComponent(); Gender gender = (Gender) genderMenu.getConvertedValue(Gender.class); if (Gender.MALE.equals(gender)) { viewState.setCurrentDestination("male"); } else { viewState.setCurrentDestination("female"); } } }
Now there is something there that needs explaining. Were do we define those names in setCurrentDestination
?
The edit
tag supports an inner tag named destination
that allows you to define destinations,
or exit point, from the generated form. So the use of the edit
tag would be a little different:
<fr:edit name="bean" action="/renderers/searchPersons.do?method=search"> <fr:destination name="female" path="/renderers/searchFemales."/> <fr:destination name="male" path="/renderers/searchMales.do"/> </fr:edit>
There is also a default behaviour that is important to know. By default there are three destination names that are used if provided:
"success"
"invalid"
"cancel"
The next example shows a controller that capitalizes the text of a field. This controller needs to
be associated to a submit button to be executed properlly. The behaviour inherited from
HtmlSubmitButtonController
makes this controller be executed only when the button is
pressed and changes the submission lifecycle to not alter the object.
class CapitalizeController extends HtmlSubmitButtonController { private HtmlSimpleValueComponent input; public CapitalizeController(HtmlSimpleValueComponent component) { this.input = component; } @Override protected void buttonPressed(IViewState viewState, HtmlSubmitButton button) { String text = this.input.getValue(); this.input.setValue(capitalize(text)); } private String capitalize(String text) { StringBuilder buffer = new StringBuilder(); char ch, prevCh; prevCh = ' '; for (int i = 0; i < text.length(); i++) { ch = text.charAt(i); if (Character.isLetter(ch) && !Character.isLetter(prevCh)) { buffer.append(Character.toUpperCase(ch)); } else { buffer.append(ch); } prevCh = ch; } return buffer.toString(); } }
ViewState
Sometimes for a more complex implementation of a controller or renderer you need to store some
values between requests of the user. The ViewState
object allows you to set attributes
that are persisted (in the client side) between requests. The view state attributes are global,
that is, if you set the attribute "a"
in one renderer then all renderers and controllers
that are executed next will see that attribute. Nevetheless the default input context implementation
tries make attributes local to each renderer. The objective is to avoid name conflicts and make
the implementation easier.
The next example shows an additional controllers that change the value of a field to it's uppercase. This controller is defined as a inner class of a renderer so it has access to the input context and consequently to the view state. It uses this fact to maintain the controller in the right state even after the values are submited and the object is changed. This controller also interacts with the previous controller to make the capitalize button visible only when the value is in lower case.
class CaseChangeController extends HtmlSubmitButtonController { private HtmlSubmitButton button; private HtmlSubmitButton capitalize; private HtmlSimpleValueComponent input; public CaseChangeController(HtmlSimpleValueComponent component, HtmlSubmitButton button, HtmlSubmitButton capitalizeButton) { this.input = component; this.button = button; this.capitalize = capitalizeButton; setupButtons(); } private void setupButtons() { this.button.setText(isUpperCase() ? "To Upper Case" : "To Lower Case"); this.capitalize.setVisible(isUpperCase()); } private boolean isUpperCase() { if (getInputContext().getViewState().getAttribute("isUpperCase") == null) { return true; } return ((Boolean) getInputContext().getViewState().getAttribute("isUpperCase")).booleanValue(); } private void setUpperCase(boolean isUpperCase) { getInputContext().getViewState().setAttribute("isUpperCase", new Boolean(isUpperCase)); } @Override protected void buttonPressed(IViewState viewState, HtmlSubmitButton button) { String text = this.input.getValue(); this.input.setValue(isUpperCase() ? text.toUpperCase() : text.toLowerCase()); setUpperCase(!isUpperCase()); setupButtons(); } }
To use this two new controllers we need to create a custom renderer but that will only be shown latter. Right now all we need to know is that a new layout was defined and associated with the implemented renderer. Now lets see the working example.
Schema
<schema name="person.create-minimal-defaults" type="net.sourceforge.fenixedu.domain.Person"> <slot name="name" layout="allow-case-change"/> </schema>
Code
<fr:edit id="case-change" name="UserView" property="person" layout="tabular" schema="person.name"/>
Result
There is another form of collection random pieces of data from the user but it probably still requires a bean to hold those random pieces. Nevertheless this new aproach allows you to have more control of some of the presentation aspects behind the renderers framework.
The idea is to allow components to be created directly from the action. This way, in the action, we can create custom components, compose them at will, and use them directly in the JSP to form the presentation. This use of components is always composed of three steps:
ViewState
MetaObject
ViewState
available in the requestLet't watch a simple but complete example and then explain the details:
Code in action
// 1 ActionViewState viewState = new ActionViewState("testing"); // 2 Schema schema = RenderKit.getInstance().findSchema("schema"); // 3 MetaObject metaObject = MetaObjectFactory.createObject(bean, schema); viewState.setMetaObject(metaObject); // 4 final HtmlTextInput input = new HtmlTextInput(); // 5 String aSlotName = schema.getSlotDescriptions().get(0).getSlotName(); input.bind(metaObject.getSlot(aSlotName)); // 6 input.setController(new HtmlController() { @Override public void execute(IViewState viewState) { System.out.println("changing the text to lowercase"); if (input.getValue() != null) { input.setValue(input.getValue().toLowerCase()); } } }); input.setConverter(new Converter() { @Override public List<String> convert(Class type, Object value) { if (value == null) { return null; } else { return Arrays.asList(((String) value).split("\\p{Space}+")); } } }); // 7 request.setAttribute("customNamesInput", viewState);
Code in JSP
<fr:viewstate name="customNamesInput" action="/example.do?method=collect"/>
Code in Action (handle submit)
ViewState viewState = RenderUtils.getViewState("testing"); HtmlTextInput component = (HtmlTextInput) viewState.getComponent(); UsedBean bean = (UsedBean) viewState.getMetaObject().getObject(); List<String> names1 = (List<String<) component.getConvertedValue(); List<String> names2 = bean.getNamesList();
As you may notice we started by creating a specific ViewState
. The ActionViewState
is a specialization of the ViewState
commonly available, that can be used directly by actions
and that skips some steps in the renderers lifecycle.
This ViewState
has the same role as the other automatically created by the fr:edit
and fr:create
tags. The essential about the ViewState
is that it must contain a
component. Optionally it can contain a meta object if some component is bound to a slot, that is, the
ViewState
must contain the meta object containing the slots to wich component were bound.
There aren't other requirements over the ViewState
. After setting it up you just need to make it
available to a JSP, usually by setting a request attribute.
When you have an object and want to associate some input components to slots of that object then you need to create an abstraction of that object called the meta object. This meta object it the one used by the renderers framework and is usefull to help serializing objects and prevent changes to object until they really need to be made.
To create a meta object you normally should obtain a schema. The schema limits the slots that the meta object
will contain but you don't really need the schema. If you pass null
as a schema then all the
object's slots are available in the meta object. Schemas can be obtained through the RenderKit
object.
Schemas are found by their name in the configuration.
After you created the meta object don't forget to set the ViewState
with it.
When you want to associate some input component's value to a slot as it normally happens in the fr:edit
and fr:create
tags you just need to select the desired meta slot from the create meta object and
bind the input component to it. After doing this, the final value of the component, after the user submited the form,
will be set in the object.
Off course, you can only by one input component to a meta slot. You can bind several input component to the same slot but all well get is having the setter being called several times.
As you may have noticed, you don't really need the bean in the example. If you don't bind any input component to a slot then you don't need to create the meta object and so you won't need the object/bean. Nevertheless you will have to obtain the value directly from the input component and probably have to take measures to make them easy to find like giving them specific ids.
You may need a bean when you want some logic associated with the retrievel or processing of the information or just to concentrate all the information in an object easy to pass to other parts of the program but it's never required.
The converter and controller you see in the example are just ther to make a point, they don't really make anything
usefull. But as you can see you can associate any piece of code that is executed afer the input of the user is
processed. This can be used, for example, to make a preprocessing of values or to copy some intermediate values to
a final location. Controllers can also change the lifecycle through the ViewState
provided so they can
be used for a lot of purposes.
As mentioned a typical use is to use the values of several fields and put a computed value in a final field that normally has converter associated. For example you can provide 3 input fields and grab those three values to make a parseable time that you put in a 4th field.
Convertes are used because all the values of the components are strings or arrays of strings. The converter allows to request the final value for the slot having the conversion been specified beforehand. The converter is specially usefull when an input component is bound to a slot. You need to add a converter whenever the value of the component cannot be trivially converter into the slot's type.
Top
Next: