The common opinion about JSF versions prior to 2.0 is that they are not well suited for POST-Redirect-GET pattern and bookmarkability. Actually the situation is not that bad. Consider the typical scenario: one page holds a list of entities, each with the link to the entity's own page; that page contains a form for entity properties editing and button to save new values; after button is clicked and entity is updated on server side, redirect is issued to the same entity page. How could this be implemented using JSF 1.2 with facelets?
Each link to the entity page will contain id as a parameter. So table listing entities with links to their page can be implemented as simple as follows:
<h:dataTable var="entity" value="#{entitieBean.entities}">
<h:column>
<a href="entity.jsf?id=#{entity.id}">#{entity.name}"/></a>
</h:column>
</h:dataTable>
<h:column>
<a href="entity.jsf?id=#{entity.id}">#{entity.name}"/></a>
</h:column>
</h:dataTable>
entitiesBean implementation is straightforward. When specific entity page is requested, id from request parameter is converted and injected into entityBean. Setter method loads target entity from database and stores it in bean aloowing to be displayed/edited on page. Here is the corresponding config in faces-config.xml:
<managed-bean>
<managed-bean-name>entityBean</managed-bean-name>
<managed-bean-class>test.jsf.EntityBean</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>entityId</property-name>
<property-class>java.lang.Integer</property-class>
<value>#{param.id}</value>
</managed-property>
</managed-bean>
<managed-bean-name>entityBean</managed-bean-name>
<managed-bean-class>test.jsf.EntityBean</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>entityId</property-name>
<property-class>java.lang.Integer</property-class>
<value>#{param.id}</value>
</managed-property>
</managed-bean>
and EntityBean.java implementation:
public class EntityBean {
private Entity entity;
public void setEntityId(Integer id) {
//load entity from database by id;
}
public Entity getEntity() {
return entity;
}
public void validateName(FacesContext context, UIComponent component, Object value) {
//validate entity name on uniquiness
}
public String save() {
//save entity changes to datatabase
return "success";//action outcome - to be mapped in navigation rules
}
}
private Entity entity;
public void setEntityId(Integer id) {
//load entity from database by id;
}
public Entity getEntity() {
return entity;
}
public void validateName(FacesContext context, UIComponent component, Object value) {
//validate entity name on uniquiness
}
public String save() {
//save entity changes to datatabase
return "success";//action outcome - to be mapped in navigation rules
}
}
on the page, entity form will look like this:
<h:form id="entityForm">
<input type="hidden" name="id" value="#{entityBean.entity.id}"/>
<h:panelGrid columns="2">
<f:facet name="footer">
<h:commandButton action="#{entityBean.save}" value="Save"/>
</f:facet>
<h:outputLabel for="name" value="Name:"/>
<h:panelGroup>
<h:inputText id="name" value="#{entityBean.entity.name}" required="true" validator="#{entityBean.validateName}"/>
<h:message for="name"/>
</h:panelGroup>
</h:panelGrid>
</h:form>
<input type="hidden" name="id" value="#{entityBean.entity.id}"/>
<h:panelGrid columns="2">
<f:facet name="footer">
<h:commandButton action="#{entityBean.save}" value="Save"/>
</f:facet>
<h:outputLabel for="name" value="Name:"/>
<h:panelGroup>
<h:inputText id="name" value="#{entityBean.entity.name}" required="true" validator="#{entityBean.validateName}"/>
<h:message for="name"/>
</h:panelGroup>
</h:panelGrid>
</h:form>
Notice hidden input populated with entity id value: this is required in order to correctly load entity on postback (that is, when "Save" button is clicked) using the same routine as for initial GET request.
So far standard features of JSF were sufficient to implement bookmarkability. The only required feature which is not supported out of the box, is redirect with query parameters. One will need to implement custom NavigationHandler and/or ViewHandler in order to be able to write navigation rules like this:
<navigation-rule>
<from-view-id>/entity.xhtml</from-view-id>
<navigation-case>
<from-action>#{entityBean.save}</from-action>
<from-outcome>success</from-outcome>
<to-view-id>/entity.xhtml?id=#{entityBean.entity.id}</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
<from-view-id>/entity.xhtml</from-view-id>
<navigation-case>
<from-action>#{entityBean.save}</from-action>
<from-outcome>success</from-outcome>
<to-view-id>/entity.xhtml?id=#{entityBean.entity.id}</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
Once this done, the described scenario is fully implemented. This solution works, hovewer, it still leaves something to be desired:
- No good way to process conversion errors on id injection
- No good way perform id validation
- Custom *Handler must be implemented
- No tool support for custom format of navigation rules
- Non-faces hidden input must be added to the forms
Side note: the main problem which arises on this way is that view parameters are poorly documented. Actually, this is case for almost all new features. In the JSF 1.1 times specification was written beautifully - one has the option to read spec and be able to fully understand what is going on. Now technology itself is much more attractive but its specification is horrible. For example, when talking about view parameters, it says the following: "The normative specification for this feature is spread out across several places... This leads to a very diffuse field of specification requirements... To aid in understanding the feature, this section provides a nonnormative overview of the feature. Consider a web application that uses this feature exclusively on every page... Every page has at least one <h:link> or <h:button> with the appropriate parameters nested within it. No other kind of navigation components are used in the application.". In other words, spec says that it fails to describe feature in readable way and in order to fix this fail it provides an example (aiming to give us a better understanding of the idea) and this example does not uses form submissions. What if you want to know what happens with view parameters during postback? Spec says nothing about it.
In order to get a better understanding I've bought the "JavaServer Faces 2.0, The Complete Reference" book, but it appears to be written as horribly as the specification itself. So the most information about JSF 2.0 may be found in various blogs and online articles. I used the great one by Dan Allen when investigating view parameters feature.
What we need to do is to add "id" parameter to the entity page. On GET request it will be processed, converted, validated and applied to the model; moreover, it will be saved in view and applied during postback without any additional actions from developer side. Looks promising, isn't it?
Entity list page will not really differ; probably one would use specialized component
<h:link outcome="entity">
<f:parameter name="id" value="#{entity.id}"/>
#{entity.name}
</h:link>
<f:parameter name="id" value="#{entity.id}"/>
#{entity.name}
</h:link>
instead of the raw <a> links. Now it's time for entity page. We add an id viewParameter to the view:
<f:metadata>
<f:viewParam id="id" name="id" value="#{entityBean.entityId}" required="true"/>
</f:metadata>
<f:viewParam id="id" name="id" value="#{entityBean.entityId}" required="true"/>
</f:metadata>
It is also possible to provide additional conversion and validation settings for view parameter but it is not required for now. Then remove hidden id param from the form in hope that it will not be needed anymore. That's all changes required for enity page.
In the page bean we make the following changes. First, add managed bean annotation (or, even better, utilize CDI). Second, add getter for entityId property; in order to provide implementation for it we also need to store value of the entityId in the bean field during setter invocation. We will also utilize implicit navigation feature of JSF 2.0 in action method. The resulting bean class looks as follows:
@ManagedBean
@RequestScoped
public class EntityBean {
private Integer entityId;
private Entity entity;
public void setEntityId(Integer id) {
entityID = id;
//load entity from database by id;
}
public Integer getEntityId() {
return entityId;
}
public Entity getEntity() {
return entity;
}
public void validateName(FacesContext context, UIComponent component, Object value) {
//validate entity name on uniquiness
}
public String save() {
//save entity changes to datatabase
return "/entity?faces-redirect=true&includeViewParams=true";//implicit navigation - no need for navigation rule
}
}
@RequestScoped
public class EntityBean {
private Integer entityId;
private Entity entity;
public void setEntityId(Integer id) {
entityID = id;
//load entity from database by id;
}
public Integer getEntityId() {
return entityId;
}
public Entity getEntity() {
return entity;
}
public void validateName(FacesContext context, UIComponent component, Object value) {
//validate entity name on uniquiness
}
public String save() {
//save entity changes to datatabase
return "/entity?faces-redirect=true&includeViewParams=true";//implicit navigation - no need for navigation rule
}
}
The last step is removing managed bean and navigation sections from faces-config.xml. The obtained solution looks much better then pre-JSF 2 one. Now it's time to test it.
When tested, entities page works well as do entity page when being accessed by GET request. But when form on entity page is submitted we get "Target unreachable" exception.
The reason for this error is that now value of the id parameter is being applied to the model not immediately after managed bean instantiation, but during Update Model Values phase of the standard JSF request processing lifecycle. This means that entity in the managed bean is null before this phase but it is required earlier - at the Process Validations phase, for converting and validating new value of the name property.
How can this error be avoided? The mentioned "JavaServer Faces 2.0, The Complete Reference" book utilizes the following approach for handling postbacks in POST,REDIRECT,GET pattern:
- Create method loadEntity(ComponentSystemEvent cse) in the backing bean, which loads entity by id into bean and also stores it in the Flash (say under "selectedEntity" name).
- Add <f:event type="preRenderView" listener="#{entityBean.loadEntity}"/> to the metadata section of the page so that this method will be executed before Render View phase.
- Add <c:if test="#{! empty flash.selectedEntity}"><c:set target="#{entityBean}" property="entity" value="#{flash.selectedEntity}"/></c:if> to the <h:form>.
- A lot of additional code must be written for such easy thing as loading entity by its id. The code is not typesafe (when dealing with Flash).
- Code and presentation are mixed (that <c:set ..." is a code which is incorporated inside view (facelet page)).
- It is hard to maintain.
- It looks ugly.
What can we do in absence of this attribute? I found the following solution working. Add validator method to the managed bean which will validate valued of the id param and move entity loading inside that method :
public void validateID(FacesContext context, UIComponent component, Object value) {
if (value != null) {
entityId = (Integer) value;
//load entity from database by id;
if (entity == null) throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
"Entity with id = " + entityId + " was not found", null));
}
}
if (value != null) {
entityId = (Integer) value;
//load entity from database by id;
if (entity == null) throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
"Entity with id = " + entityId + " was not found", null));
}
}
And associate this method with id parameter:
<f:viewParam name="id" value="#{entityBean.countryId}" required="true" validator="#{entityBean.validateID}"/>
Using this solution, entity will be loaded at the Process Validation phase and it will be loaded before processing of the other form components during thesame phase (because view parameter was defined earlier in the page).
While this solution works it still not ideal because loading entity during validation is not what validation designed for. Let's hope that future versions of JSF provide us with some better way for utilizing view parameters during postbacks.
No comments:
Post a Comment