Illegal Syntax for Set Operation: How to tell JSF

2019-06-17 02:31发布

问题:

This question may be more of the type "conceptual" or "I don't understand JSF".

My scenario: I have a JSF Page (index.xhtml) where I use a p:accordionPanel (but I don't think it matters what component it is). What I want to do is to set the activeIndexes of it.

<p:accordionPanel multiple="true" activeIndex="#{myController.getActiveIndexesForSections('whatever')}">
// bla bla...
</p:accordionPanel>

And the (simplified) method in the backing bean:

public String getActiveIndexesForSections(String holderName){
    String activeSections = "";
    for(Section s : sectionMap.get(holderName)){
        if (s.isActive())
        //add to the string
    }
    return activeSections;
}

Now this works just fine on a normal page load.

But if I click on a p:commandButton (with ajax=false) (or anything else which "sends" data back to the server I guess) - I get the following exception:

/WEB-INF/tags/normalTextSection.xhtml @8,112 activeIndex="#{myController.getActiveIndexesForSections(name)}": Illegal Syntax for Set Operation
// bla..
Caused by: javax.el.PropertyNotWritableException: Illegal Syntax for Set Operation

After some googling / reading the error message I found that I need a setter.

First of all: I don't want a setter - do I really need one or is there a way to tell JSF I don't want this "behavior".

Second I realized that it's not that "easy" to provide a setter, because my method has a parameter (so public void setActiveIndexesForSections(String name, String activeIndexes) or public void setActiveIndexesForSections(String name)won't work). What I came up with in the end is:

Create a (generic) "Pseudo-Property-class":

// just a dummy class since the class is recreated at every request
public class Property<T> implements Serializable {

    private T val;

    public Property(T val) {
        this.val= val;
    }

    public T getVal() {
        return val;
    }

            //no need to do anyhting
    public void setVal(T val) {
    }
}

Change the bean method:

public Property<String> getActiveIndexesForSections(String holderName){
    String activeSections = "";
    for(Section s : sectionMap.get(holderName)){
        if (s.isActive())
        //add to the string
    }
    return new Property<String>(activeSections);
}

And call it from the index.xhtml:

<p:accordionPanel multiple="true" activeIndex="#{myController.getActiveIndexesForSections('whatever').val}">
// bla bla...
</p:accordionPanel>

This works but obviously is a ugly hack/workaround.

What is the proper way to handle a situation like this? Or is what I'm doing simply completely wrong?

回答1:

The setter is needed to remember the active indexes as they were when the form is submitted. Basically, you need to bind it as a value expression (with a property), not as a method expression (like an action method), nor to an unmodifiable collection (like activeIndex="#{param.tab}"). Exactly like as with input values. Technically, you're indeed doing it "simply completely wrong" ;)

The requirement is however understood. Given that you're really not interested in the changed active indexes, and thus want to reset them to defaults on every form submit, then you can bypass it by storing the result as a request attribute with help of <c:set>. This way you will fool EL to set it in the request attribute map instead of the intented bean property.

<c:set var="activeIndex" value="#{myController.getActiveIndexesForSections('whatever')}" scope="request" />
<p:accordionPanel multiple="true" activeIndex="#{activeIndex}">
    <!-- bla bla... -->
</p:accordionPanel>

Under the covers, it will basically do externalContext.getRequestMap().put("activeIndex", value) as setter operation, which will obviously just work.


Update: upon inspecting the source code of AccordionPanel component, I saw another workaround given the fact that the activeIndex won't be set when the rendered attribute evaluates false. So just alter the rendered attribute to behave exactly that: evaluate false during update model values phase (the 4th phase).

<p:accordionPanel multiple="true" 
    activeIndex="#{myController.getActiveIndexesForSections('whatever')}"
    rendered="#{facesContext.currentPhaseId.ordinal ne 4}">
    <!-- bla bla... -->
</p:accordionPanel>