4.1.1.2. Navigation

4.1.1.2. Navigation

You can use standard JSF navigation rules defined in faces-config.xml in a Seam application. However, JSF navigation rules have a number of annoying limitations:

A further problem is that "orchestration" logic gets scattered between pages.xml and faces-config.xml. It's better to unify this logic into pages.xml.

This JSF navigation rule:

<navigation-rule>
    <from-view-id>/editDocument.xhtml</from-view-id>
    
    <navigation-case>
        <from-action>#{documentEditor.update}</from-action>
        <from-outcome>success</from-outcome>
        <to-view-id>/viewDocument.xhtml</to-view-id>
        <redirect/>
    </navigation-case>
    
</navigation-rule>

Can be rewritten as follows:

<page view-id="/editDocument.xhtml">
    
    <navigation from-action="#{documentEditor.update}">
        <rule if-outcome="success">
            <redirect view-id="/viewDocument.xhtml"/>
        </rule>
    </navigation>
    
</page>

But it would be even nicer if we didn't have to pollute our DocumentEditor component with string-valued return values (the JSF outcomes). So Seam lets us write:

<page view-id="/editDocument.xhtml">
    
    <navigation from-action="#{documentEditor.update}" 
                   evaluate="#{documentEditor.errors.size}">
        <rule if-outcome="0">
            <redirect view-id="/viewDocument.xhtml"/>
        </rule>
    </navigation>
    
</page>

Or even:

<page view-id="/editDocument.xhtml">
    
    <navigation from-action="#{documentEditor.update}">
        <rule if="#{documentEditor.errors.empty}">
            <redirect view-id="/viewDocument.xhtml"/>
        </rule>
    </navigation>
    
</page>

The first form evaluates a value binding to determine the outcome value to be used by the subsequent rules. The second approach ignores the outcome and evaluates a value binding for each possible rule.

Of course, when an update succeeds, we probably want to end the current conversation. We can do that like this:

<page view-id="/editDocument.xhtml">
    
    <navigation from-action="#{documentEditor.update}">
        <rule if="#{documentEditor.errors.empty}">
            <end-conversation/>
            <redirect view-id="/viewDocument.xhtml"/>
        </rule>
    </navigation>
    
</page>

But ending the conversation loses any state associated with the conversation, including the document we are currently interested in! One solution would be to use an immediate render instead of a redirect:

<page view-id="/editDocument.xhtml">
    
    <navigation from-action="#{documentEditor.update}">
        <rule if="#{documentEditor.errors.empty}">
            <end-conversation/>
            <render view-id="/viewDocument.xhtml"/>
        </rule>
    </navigation>
    
</page>

But the correct solution is to pass the document id as a request parameter:

<page view-id="/editDocument.xhtml">
    
    <navigation from-action="#{documentEditor.update}">
        <rule if="#{documentEditor.errors.empty}">
            <end-conversation/>
            <redirect view-id="/viewDocument.xhtml">
                <param name="documentId" value="#{documentEditor.documentId}"/>
            </redirect>
        </rule>
    </navigation>
    
</page>

Null outcomes are a special case in JSF. The null outcome is interpreted to mean "redisplay the page". The following navigation rule matches any non-null outcome, but not the null outcome:

<page view-id="/editDocument.xhtml">
    
    <navigation from-action="#{documentEditor.update}">
        <rule>
            <render view-id="/viewDocument.xhtml"/>
        </rule>
    </navigation>
    
</page>

If you want to perform navigation when a null outcome occurs, use the following form instead:

<page view-id="/editDocument.xhtml">
    
    <navigation from-action="#{documentEditor.update}">
        <render view-id="/viewDocument.xhtml"/>
    </navigation>
    
</page>