Fluent Navigation in JSF 2
In this article, the third in a series covering JavaServer Faces (JSF) 2.0 features contributed by Red Hat, or which Red Hat participated in extensively, you'll discover that getting around in a JSF 2 application is much simpler and requires less typing. With improved support for GET requests and bookmarkability, which the previous article covered, JSF 2 is decidely more nimble. But not at the cost of good design. JSF no longer has to encroach on your business objects by requiring action methods to return navigation outcomes, but can instead reflect on the state of the system when selecting a navigation case. This article should give you an appreciation for how intelligent the navigation system has become in JSF 2.
Read the other parts in this article series:
Part 1 - JSF 2: Seam's Other Avenue to Standardization
Part 2 - JSF 2 GETs Bookmarkable URLs
Three new navigation
variants are going to be thrown at you in this article: implicit,
conditional and preemptive. These new options are a sign that the JSF
navigation system is becoming more adaptable to the real world. There's
also a touch of developer convenience thrown in.
Implicit
navigation is particularly useful for developing application
prototypes, where navigation rules just get in the way. This style of
navigation interprets navigation outcomes as view IDs. As you move
beyond prototyping, conditional navigation removes the coupling between
the web and transactional tier because the navigation handler pulls
information from your business components to select a navigation case.
Preemptive navigation,
which you were introduced to in the last article, can use either
implicit navigation or declarative navigation rules to produce
bookmarkable URLs at render time. Leveraging the navigation system to
generate bookmarkable URLs allows JSF to add GET support while
maintaining consistent, centralized navigation rules.
Even
with these new options, there's no telling what requirements your
application might have for navigation. Thus, in JSF 2, you can finally
query and modify the navigation cases; a new API has been introduced in
JSF 2 that exposes the navigation rule set. Before we get into
customizations, let's find out how these new variants make the
navigation system more flexible and help prepare the user's next move.
Hopefully you won't need those customizations after all.
Flexible navigation choices
The declarative navigation model in JSF was a move away from the
explicit navigation "forward" selection by the action in Struts.
Navigation transitions in JSF, which get matched based on current view
ID, logical outcome and/or action expression signature, are described
in the JSF descriptor (faces-config.xml) using XML-based rules. The
matched transition indicates the next view to render and whether a
client-side redirect should proceed rendering. Here's a typical example:<navigation-rule>
<from-view-id>/guess.xhtml</from-view-id>
<navigation-case>
<from-action>#{numberGuessGame.guess}</from-action>
<from-outcome>correct</from-outcome>
<to-view-id>/gameover.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
While the JSF navigation model is clearer and arguably more flexible than in Struts, two fundamental problems remain. First, the action method is still required to return a navigation directive. The directive just happens to be a more "neutral" string outcome rather than an explicit type (i.e., ActionForward), but the coupling is just as tight and you loose type safety in the process, so is it really an improvement? The other issue is that you must define a navigation case to match that outcome, even in the simplest cases, which can be really tedious. So you can't make the argument that the navigation model is less obtrusive or more convenient. It's just stuck somewhere in between.
To sum it up, the JSF navigation model is not flexible enough. It needs to accommodate different development styles better and it needs to be more self sufficient. On the one hand, your style or development phase may dictate waiving the declarative navigation rule abstraction. On the other hand, you may want to completely decouple your business objects from the navigation model, eradicating those arbitrary return value directives. JSF 2 gives you this broad range of options, and even let's you settle for a happy medium. The first option is provided by implicit navigation and the second conditional navigation. With implicit navigation, you can even use the current model without having to define the navigation rule right away. Let's unbox these two new alternatives, starting with implicit navigation.
Implicit navigation
JSF will post a form back to the current view (using the POST HTTP method) whenever the user performs an action, such a clicking a command button (hence the term "postback"). In the past, the only way to get JSF to advance to another view after the action is invoked (i.e., following the Invoke Application phase) was to define a navigation case in faces-config.xml. Navigation cases are matched based on the EL signature of the action method invoked and the method's return value converted to a string (the logical outcome). To cite an example, assume the user clicks on a button defined as follows:<h:commandButton action="#{commandHandler.preview}" value="Preview"/>
The preview() method on the bean named commandHandler returns a value to indicate the outcome of processing:
public String preview() {
// tidy, translate and/or validate comment
return "success";
}These two criteria are joined in a navigation case that dictates which view is to be rendered next.
<navigation-rule>
<from-view-id>/entry.xhtml</from-view-id>
<navigation-case>
<from-action>#{commentHandler.preview}</from-action>
<from-outcome>success</from-outcome>
<to-view-id>/previewComment.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
If no navigation case can be matched, all JSF knows to do is render the current view again. So without a navigation case, there is no navigation.
A quick shorthand, which is present in Seam, is to have the action method simply return the target view ID directly. In this case, you're effectively treating the logical outcome value as a view ID. This technique has been adopted in JSF 2 as implicit navigation. It's improved since Seam because you can choose to drop the view extension (e.g., .xhtml) and JSF will automatically add it back on for you when looking for a view ID. Therefore, it's no more invasive than the string outcome values you are currently returning.
Implicit navigation comes into play when a navigation case cannot be matched using the existing mechanism. Here's how the logic outcome is processed in the implicit navigation case:
-
Detect the presence of the ? character in the logical outcome
- If present, capture the query string parameters that follow it the ? character
- The special query string parameter faces-redirect=true indicates that this navigation should be issued using a client-side redirect
- If the logical outcome does not end with a file extension, append file extension of current view ID (e.g., .xhtml)
- If the logical outcome does not begin with a /, prepend the location of current view id (e.g., /, /admin/, etc.)
-
Attempt to locate the template for the view ID
- If the template is found, create a virtual navigation case that targets the resolved view ID
- If the template is not found, skip implicit navigation
-
Carry out the navigation case
- If the navigation case is not a redirect, build and render the target view in the same request
- If the navigation case is a redirect, build a redirect URL, appending the query string parameters captured earlier, then redirect to it
Implicit navigation can be leveraged anywhere a logical outcome is interpreted. That includes:
- The return value of an action method
- The action attribute of a UICommand component (e.g., <h:commandButton action="/entries.xhtml" ...>)
- The outcome attribute of a UIOutcomeTarget (e.g., <h:link outcome="/entries.xhtml" ...>)
- The handleNavigation() method of the NavigationHandler API
Here's an example of the navigation to the preview comment view translated into implicit navigation. The return value is automatically decorated with a leading / and a trailing .xhtml.
public String preview() {
// tidy, translate and/or validate comment
return "previewComment";
}The /previewComment.xhtml view will be rendered in the same request. If you want to redirect first, add the following flag in the query string of the return value:
public String preview() {
// tidy, translate and/or validate comment
return "previewComment?faces-redirect=true";
}
You can accomplish any navigation scenario using implicit navigation that you can today with a formal navigation case defined in faces-config.xml. Implicit navigation is designed as the fall-through case (after the explicit navigation rules are consulted). If it fails (i.e., the template cannot be located), and the JSF 2 ProjectStage is set to development, a FacesMessage is automatically generated to warn the developer of a possible programming error.
Implicit navigation is great for prototyping and other rapid development scenarios. The major downside of implicit navigation is that you are further tying your business objects into the navigation model. Next we'll look conditional navigation, which provides an alternative that keeps your tiers loosely coupled.
Conditional navigation
Implicit navigation spotlights how invasive it is to put the onus on your business object to return a logic outcome just to make JSF navigation happy (and work). This coupling is especially problematic when you want to respond to user interface events using components in your business tier, a simplified architecture that is supported by both Seam and Java EE 6 to reduce the amount of glue code without increasing coupling.What would be more "logical" is to invert the control and have the navigation handler consult the state of the bean to determine which navigation case is appropriate. The navigation becomes contextual rather than static. That's what conditional navigation gives you.
Conditional navigation introduces a condition as a new match criteria on the navigation case. It's defined in the <if> element as a child of <navigation-case> and expressed using an EL value expression. The value expression is evaluated each time the navigation case is considered. For any navigation case that matches, if a condition is defined, the condition must resolve to true for the navigation case to be considered a match.
Here's an example of a conditional navigation case:
<navigation-case>
<from-action>#{registration.register}</from-action>
<if>#{currentUser.registered}</if>
<to-view-id>/account.xhtml</to-view-id>
<redirect/>
</navigation-case>
As you can see, the condition doesn't necessarily have to reference a property on the bean that was invoked. It can be any state reachable by EL.
Conditional navigation solves a secondary problem with the JSF navigation model, one of those little annoyances in JSF that was tedious to workaround. In JSF 1.2 and earlier, if your action method is a void method or returns a null value, interpreted in both cases as a null outcome, the navigation is skipped entirely. As a result, the current view is rendered again. The only workaround is to override the navigation handler implementation and change the behavior. That really throws a wrench in being able to cut the glue code between your UI and transactional tier.
That changes with the introduction of conditional navigation. Since the condition provides either an alternative, or supplemental, match criteria to the logical outcome, navigation cases that have a condition are consulted even when the logical outcome is null or void. When the outcome is null, you can emulate switch statement to match a navigation case, switching on the condition criteria:
<navigation-case>
<from-action>#{identity.login}</from-action>
<if>#{currentUser.admin}</if>
<to-view-id>/admin/home.xhtml</to-view-id>
<redirect/>
</navigation-case>
<navigation-case>
<from-action>#{identity.login}</from-action>
<if>#{currentUser.vendor}</if>
<to-view-id>/vendor/home.xhtml</to-view-id>
<redirect/>
</navigation-case>
<navigation-case>
<from-action>#{identity.login}</from-action>
<if>#{currentUser.client}</if>
<to-view-id>/client/home.xhtml</to-view-id>
<redirect/>
</navigation-case>
If you intend to simply match the null outcome in any case, you can use a condition that is verily true (which, admittedly, could be improved in JSF 2.1):
<navigation-case>
<from-action>#{identity.logout}</from-action>
<if>#{true}</if>
<to-view-id>/home.xhtml</to-view-id>
<redirect/>
</navigation-case>
You can also use this fixed condition to provide a fall-through case.
But wait, there's more! Having to itemize all the possible routes using individual navigation cases causes death by XML (a quite painful death). What if you wanted to delegate the decision to a navigation helper bean or involve a scripting language? There's good news. You can! The target view ID can be resolved from an EL value expression. Let's return to the login example and use a helper bean to route the user using one navigation case:
<navigation-case>
<from-action>#{identity.login}</from-action>
<to-view-id>#{navigationHelper.userHomeViewId}</to-view-id>
<redirect/>
</navigation-case>
Oh my goodness, how much nicer is that? The navigation helper can encapsulate the logic of inspecting the currentUser bean and determining the correct target view ID.
In this section, we looked at two additional ways a navigation case is matched, increasing the overall flexibility of the navigation model. Implicit navigation maps logical outcomes directly to view IDs and conditional navigation reflects on contextual data to select a navigation case without imposing unnecessary coupling with the transactional tier. We're still looking at the same fundamental navigation model, though. In the next section, you'll see the navigation model used in a new role, and in a new place in the JSF life cycle, to generate bookmarkable links.
- Login or register to post comments
- 4553 reads
- Printer-friendly version
(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)










