Illegal Syntax for Set Operation: How to tell JSF I don't "want" a setter
Asked Answered
T

1

9

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?

Talmud answered 23/6, 2014 at 16:30 Comment(0)
G
23

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>
Greening answered 25/6, 2014 at 19:24 Comment(5)
Thanks for having mercy on me ;) Would you recommend another way to do it - since it's "simply" wrong? (What I can't believe is, that this use case is so rare that nobody (expect me) ever wanted to do this - so I'm indeed wondering if I missunderstood the big picture...) - Also my accordionPanels are generated within a tag where 'whatever' is passed in as a parameter - can I even use the parameter as thevarof the c:set and then as the activeIndex of the accordionPanel? If so how?Sev
The right way in this specific case would be returning a custom Map implementation. And no, you can't use EL in var attribute, so a tagfile would be nasty. In any way, after inspecting the source code of <p:accordionPanel>, I saw another workaround which is better reusable for in tagfiles. See the answer update.Greening
Thanks. Will try this & also the 'custom' map tomorrow. And if it works accept + "bounty" you :)Sev
You're welcome. I can't flesh out an answer on the custom Map implementation as the question don't contain all the necessary information about that Section thing. But to the point, you should be able to use it in this form activeIndex="#{myController.activeIndexesForSections[whateverVariable]}" where getActiveIndexForSections() returns a Map<String, String> and whateverVariable is thus that variable representing the key. Respectively the Map#get() and Map#put() would be invoked on get and set operation. You need to override those methods to return and set the desired value.Greening
Well if you want I can give you the information - or even my code and you review it ;-) Anyways: Both solutions you provided, o indeed work - so thanks again for that. I tried the map (gist.github.com/anonymous/ea9a9afd46a11ecdbf95) - I'm not really sure if I'm happy with it - I don't really want to move my logic from the cookieController inside the map class, since the set-cookie method (which happens on the tabChange-Event) still takes place inside the cookie controller. I tend to use the rendered method you mentioned in your update...Sev

© 2022 - 2024 — McMap. All rights reserved.