jsfjsf-2.2viewaction

Invoke f:viewAction conditionally based on f:viewParam


We have a setting where we have different optional view params passed to JSF pages and subsequent view actions to be processed after the params have been set. A very simple example is shown below:

page.xhtml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:h="http://xmlns.jcp.org/jsf/html"
  xmlns:f="http://xmlns.jcp.org/jsf/core">

<f:view>
    <f:metadata>
        <f:viewParam name="a" value="#{page.a}"/>
        <f:viewAction action="#{page.populateA()}" if="#{not empty page.a}"/>

        <f:viewParam name="b" value="#{page.b}"/>
        <f:viewAction action="#{page.populateB()}"/>
    </f:metadata>

    <h:outputLabel value="#{page.message}"/>
</f:view>
</html>

Page

import javax.faces.view.ViewScoped;
import javax.inject.Named;

@ViewScoped
@Named
public class Page {

    private String a;

    private String b;

    private String message;

    public String getA() {
        return a;
    }

    public void setA(String a) {
        this.a = a;
    }

    public String getB() {
        return b;
    }

    public void setB(String b) {
        this.b = b;
    }

    public String getMessage() {
        return message;
    }

    public void populateA() {
        this.message = "Param a given: " + this.a;
    }

    public void populateB() {
        if (this.b != null) {
            this.message = "Param b given: " + this.b;
        }
    }
}

Now, the difference is that the processing of a does not work (page.xhtml?a=123), whilst the processing of b works like a charm (page.xhtml?b=123) - even though I thought I simply moved the null-check from Java to JSF. In respect of readability I would prefer to omit additional null-checks in Java and have the view param processing fully located in JSF, but how can I tweak the code to make the first scenario work?

EDIT According to the accepted answer of What is the purpose of rendered attribute on f:viewAction?, if works per se so I suspect a wrong execution order (first evaluate the action condition, then apply the values to the model).


Solution

  • The <f:viewAction if> attribute is basically a renamed <h:xxx rendered> attribute (I'll leave in the middle whether that's more clear or not, it has at least a bug in autogenerated documentation as consequence; it lists rendered instead of if) and has thus exactly the same lifecycle during among others the Apply Request Values phase as all other UI components. This phase calls the UIComponent#decode() of all UI components in the view. For the UIViewAction, the UI component class behind <f:viewAction> tag, the decode() is described as follows:

    Take no action if any of the following conditions are true:

    • The current request is a postback and the instance has been configured to not operate on postback. See isOnPostback().

    • The condition stated in the if property evaluates to false. See isRendered()

    So, yes, you're indeed right as to "wrong execution order". During Apply Request Values phase it checks the if attribute and if it evaluates false, then the decode won't take place and the action event won't be queued. See also line 609 of UIViewAction source code where it tests isRendered() inside decode() and doesn't proceed if false. In your case, the #{page.a}, which you're checking in the <f:viewAction if>, is only available during Update Model Values phase, which is after the Apply Request Values phase.

    You'd like to test something else which is guaranteed to be available during Apply Request Values phase. The best candidate would be the actual request parameter itself. All "plain" request parameters are in EL scope available by the implicit EL object #{param} which references a Map<String, String> with the parameter name as key.

    So, all in all, this should do:

    <f:viewParam name="a" value="#{page.a}"/>
    <f:viewAction action="#{page.populateA}" if="#{not empty param.a}"/>
    

    Update to solve the unintuitive behavior of <f:viewAction if>, JSF utility library OmniFaces will since version 2.2 offer an <o:viewAction> whose if is only evaluated during Invoke Application phase, right before the action event is broadcasted. This trick was done by simply extending the UIViewAction component and overriding its broadcast() and isRendered() as below:

    @Override
    public void broadcast(FacesEvent event) throws AbortProcessingException {
        if (super.isRendered()) {
            super.broadcast(event);
        }
    }
    
    @Override
    public boolean isRendered() {
        return !isImmediate() || super.isRendered();
    }
    

    This allows you to use the tag more intuitively:

    <f:viewParam name="a" value="#{page.a}"/>
    <o:viewAction action="#{page.populateA}" if="#{not empty page.a}"/>