Overview
One of the most common questions about application development in JavaServer Faces (JSF) is how to communicate values between pages. For example, how do I pick one row out of a table, and show it on a second page to start editing it? How do I take search criteria entered on one page, and show the results on a second? Both of these (and many other common web application scenarios) require some mechanism to pass a value from one page to another. The two common solutions in JSF have been storing values on the request or on the session. Both can be made to work, but both have major limitations. Apache Trinidad introduces a new "pageFlowScope" that seeks to offer the best of both. (This support is built entirely off of existing, specified hooks in the Faces specification, and is not based on proprietary extensions. If there is interest from the rest of the Faces expert group, we would be very interested in contributing this back into the specification.)
When using a request-scope value, an action listener might take the ID of that table row, or the collection of search results, and place that object into request scope, either directly:
FacesContext context = FacesContext.getCurrentInstance(); context.getExternalContext().getRequestMap().put("search", criteria);
... or indirectly, by storing it as a property of a request-scoped <managed-bean>. This works, but has some significant drawbacks:
- The <navigation-case> used for page navigation can't specify <redirect/>, because that would lead to a client-side redirect, which would mean the second page gets rendered on a new request. <redirect/> is very useful for supporting bookmarking and better Back button support.
- Even if you don't redirect, the second page still has the problem of making sure that this request-scoped value is still available for its own purposes when it posts back.
To avoid these problems, developers might use session-scoped variables instead. This fixes both of those problems, but adds new ones:
- A single user cannot have two windows open simultaneously; session-scoped variables are global to the user. So, for instance, a user could not work with two different search results simultaneously.
- Back button support is highly limited, since navigating back can't magically restore the session to its old state.
Finally, both session- and request-scoped parameters make bookmark support completely hopeless; similarly, they make it very difficult if you need to support emailing a link to a page. The URL does not and cannot contain enough information on its own to show a target page.
Apache Trinidad offers a new scope - "pageFlowScope" - that aims to solve all of these problems. It's a very new and experimental feature, and we're interested in feedback on how well it addresses the problem (it's not a panacea - some limitations are described below).
pageFlowScope
In addition to the standard JSF scopes - applicationScope, sessionScope, and requestScope - Apache Trinidad adds a new scope, pageFlowScope. Values added to this scope will automatically continue to be available as the user continues navigating from one page to another. This is true even if you use <redirect/>. But unlike session scope, these values are only visible in the current "page flow". If the user opens up a new window and starts navigating, that series of windows will have their own page flow, and values stored in each window will remain independent. And clicking the Back button will automatically reset the page flow scope to its original state.
From the JSF EL, it looks just like any other scope:
<h:outputText value="#{pageFlowScope.someKey}"/>
From Java code, you can access the page flow scope as a java.util.Map off of the RequestContext API. (Despite its name, this class does not extend FacesContext, but it is a similar idea.)
import org.apache.myfaces.trinidad.context.RequestContext; public class BackingBean { public String myAction() { Object someValue = ...; RequestContext requestContext = RequestContext.getCurrentInstance(); requestContext.getPageFlowScope().put("someKey", someValue); return "myOutcome"; } }
Example:
Let's start with an <h:dataTable> showing some data:
<h:dataTable var="employee" value="#{....}"> ... <h:column> <h:outputText value="#{employee.name}"/> <h:column> <h:column> <h:outputText value="#{employee.id}"/> <h:column> </h:dataTable>
Now, we want to show more information about that employee on another detail page. We'll add a commandButton, and tie it to a "showEmployee" action in our backing bean:
<h:dataTable var="employee" value="#{....}"> ... <h:column> <h:outputText value="#{employee.name}"/> <h:column> <h:column> <h:outputText value="#{employee.id}"/> <h:column> <h:column> <h:commandButton value="Show more" action="#{backingBean.showEmployee}"/> <h:column> </h:dataTable>
Now all we've got to do is write the code for showEmployee(). First, we'll find the current employee, and then we'll put it onto the page flow scope.
import javax.faces.context.FacesContext; import org.apache.myfaces.trinidad.context.RequestContext; public class BackingBean { public String showEmployee() { // Find the current employee. I'll just look on the VariableResolver. // (A lot of code out on the web manually creates a ValueBinding // for "#{employee}" and executes it - this is a much simpler approach! FacesContext context = FacesContext.getCurrentInstance(); Employee emp = (Employee) context.getVariableResolver().resolveVariable(context, "employee"); if (emp == null) return "error"; RequestContext requestContext = RequestContext.getCurrentInstance(); requestContext.getPageFlowScope().put("detailEmployee", emp); // Navigate to whatever page handles the "showEmployee" outcome return "showEmployee"; } }
If you owned the code for "Employee", you might consider moving showEmployee() directly onto Employee, in which case the code would simply be:
import org.apache.myfaces.trinidad.context.RequestContext; public class Employee { ... public String showEmployee() { RequestContext requestContext = RequestContext.getCurrentInstance(); // No need to find the employee - it's "this" requestContext.getPageFlowScope().put("detailEmployee", this); // Navigate to whatever page handles the "showEmployee" outcome return "showEmployee"; } }
Now, on our detail page, we can just refer to the "pageFlowScope.detailEmployee" object:
<h:panelGrid columns="2"> <h:outputText value="Name:"/> <h:outputText value="#{pageFlowScope.detailEmployee.name}"/> <h:outputText value="Employee ID:"/> <h:outputText value="#{pageFlowScope.detailEmployee.id}"/> <h:outputText value="Salary"/> <h:outputText value="#{pageFlowScope.detailEmployee.salary}"> <f:convertNumber type="currency"/> <h:outputText> </h:panelGrid>
That's all there is to it. The detail page does need to know where to look for the incoming value. The "detailEmployee" object also persists automatically at pageFlowScope if there were a "Show Even More Details" button on this detail page.
Limitations of pageFlowScope
Before moving on, there are, however, a number of limitations of pageFlowScope.
First, since pageFlowScope is not part of the standard JSF specification, a couple of the niceties of standard scopes can't be supported:
- EL expressions do not automatically look into pageFlowScope; if you wish to locate a page flow-scoped value, you must include "pageFlowScope." (For instance, in the previous example, we couldn't write "#{employeeDetail}" - we had to write "#{pageFlowScope.employeeDetail}".)
- "page flow" cannot be used as a <managed-bean-scope>. (But the <value> of a <managed-property> can refer to page flow-scoped values.)
Second, because the original and detail pages have to agree on the name of the page flow-scoped variable, they are more tightly coupled than is ideal.
Finally, pageFlowScope never empties itself; the only way to clear pageFlowScope is to manually force it to clear:
RequestContext requestContext = RequestContext.getCurrentInstance(); requestContext.getPageFlowScope().clear();
tr:setActionListener
This code is easy, but to make coding even simpler, we provide a new ActionListener tag that lets you code this style without writing any Java code. The <tr:setActionListener> tag has two properties, "from" and "to", and simply takes a value from the "from" attribute and puts it where "to" says to put it. Let's recode the last example with this tag:
<h:dataTable var="employee" value="#{....}"> ... <h:column> <h:outputText value="#{employee.name}"/> <h:column> <h:column> <h:outputText value="#{employee.id}"/> <h:column> <h:column> <h:commandButton value="Show more" action="showEmployee"> <tr:setActionListener from="#{employee}" to="#{pageFlowScope.employeeDetail}"/> </h:commandButton> <h:column> </h:dataTable>
And that's it! No code is required in your backing bean at all to implement this pattern. Let's walk through how this page works when the button is clicked:
- An ActionEvent fires on the commandButton. Since it's inside a table row, the "employee" EL variable is pointing at the current row.
- ActionListeners execute before any "action" excute, so <tr:setActionListener> executes.
- <tr:setActionListener> retrieves the employee using the "#{employee}" EL expression.
- <tr:setActionListener> stores the employee into page flow scope with the "#{pageFlowScope.employeeDetail}" EL expression.
- Finally, the "action" executes with a static outcome - always show the employee. No backing bean is needed.
Some may point out that this tag amounts to putting code in the UI, and that is actually quite true. It is a matter of personal taste whether this style of coding is acceptable or not, but used sparingly, it can greatly simplify reading and understanding page logic. We would never recommend using <tr:setActionListener> to write values into a true model object. For example, you could write:
<h:dataTable var="employee" value="#{....}"> ... <h:column> <h:outputText value="#{employee.name}"/> <h:column> <h:column> <h:outputText value="#{employee.id}"/> <h:column> <h:column> <h:commandButton value="Give Raise"> <tr:setActionListener from="#{employee.salary + 500}" to="#{employee.salary}"/> </h:commandButton> <h:column> </h:dataTable>
Bookmarking support
The current implementation of pageFlowScope adds a single query parameter to your URL, for example, "?_afPfm=4". This token points into internal structures stored by Apache Trinidad at session scope. The strategy allows pageFlowScope to store objects of any type, but does nothing to help with bookmarking (the tokens are not persistent across requests or users). In future versions of Apache Trinidad, wemay support an augmented strategy that will detect when pageFlowScope contains nothing but primitive objects - such as strings, java.lang.Integers - and automatically store these values directly on the URL. This can directly and transparently enable bookmarkability (especially if you use <redirect/> in <navigation-case>) for web developers using pageFlowScope with that restriction.