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.