6

In my JSF view I have a p:selectCheckboxMenu where I want to perform some business logic via AJAX on the selected values.

For a simple change event it works fine, but for a toggleSelect event not. Inside my listener method I am retrieving the old selection, but I am expecting the new selection here.

See the following example:

@ViewScoped
@Named
public class RequestBean implements Serializable {
    private List<String> list; // + getter/setter

    @PostConstruct
    private void init() {       
        list = new ArrayList<String>() {{
          add("one");  add("two"); add("three");
        }};
    }

    public void listener() {
        System.out.println("Current content of \"list\":");
        for(String s : list) {
            System.out.println(s);
        }
    }
}

in JSF view:

<p:selectCheckboxMenu value="#{requestBean.list}" label="List">
    <f:selectItem itemValue="one" itemLabel="one"/>
    <f:selectItem itemValue="two" itemLabel="two"/>
    <f:selectItem itemValue="three" itemLabel="three"/>
    <p:ajax event="toggleSelect" listener="#{requestBean.listener}"  />
    <p:ajax event="change" listener="#{requestBean.listener}" />
</p:selectCheckboxMenu>

Now lets consider the following use-case: You are entering the view, "one" and "two" are selected. If I click the "select all" checkbox, the outcome is:

Info:   Current content of "list":
Info:   one
Info:   two

But the expected outcome would look like this:

Info:   Current content of "list":
Info:   one
Info:   two
Info:   three

For the regular change event it works as expected. Here I am getting the new selection inside the listener. How may I fix it? Or what am I doing wrong?


GlassFish 4.1, running on Java 1.8.0_45

JSF 2.2.10 (Mojarra)

PrimeFaces 5.1

OmniFaces 1.8.1

stg
  • 2,757
  • 2
  • 28
  • 55

6 Answers6

4

This issue seems to be related with the listener being called too early. Doing some basic debugging, I've found that toggleSelect invokes the listener method before updating the model values, while the change event does it after modifying them. That's my current code:

RequestBean:

@ViewScoped
@ManagedBean
public class RequestBean implements Serializable {
    public List<String> getList() {
        return list;
    }

    public void setList(List<String> list) {
        this.list = list;
        System.out.println("Values set: " + list);
    }

    private List<String> list;

    @PostConstruct
    private void init() {
        list = new ArrayList<String>() {
            {
                add("one");
                add("two");
                add("three");
            }
        };
    }

    public void listener() {
        System.out.println("Listener called!");
    }
}

page.xhtml:

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:p="http://primefaces.org/ui"
    xmlns:comp="http://java.sun.com/jsf/composite/comp"
    xmlns:ui="http://java.sun.com/jsf/facelets">
<h:head />
<h:body>
    <h:form>
        <p:selectCheckboxMenu value="#{requestBean.list}" label="List">
            <f:selectItem itemValue="one" itemLabel="one" />
            <f:selectItem itemValue="two" itemLabel="two" />
            <f:selectItem itemValue="three" itemLabel="three" />
            <p:ajax event="toggleSelect" listener="#{requestBean.listener}" />
            <p:ajax event="change" listener="#{requestBean.listener}" />
        </p:selectCheckboxMenu>
    </h:form>
</h:body>
</html>

And that's the trace for your current steps:

Values set: [one]
Listener called!
Values set: [one, two]
Listener called!
Listener called!
Values set: [one, two, three]

The last one is the toogle selection, as you can see the model is properly updated, but the listener is called before.

Let's play a little bit more with a custom PhaseListener:

Entering RESTORE_VIEW 1
Entering APPLY_REQUEST_VALUES 2
Entering PROCESS_VALIDATIONS 3
Entering UPDATE_MODEL_VALUES 4
Values set: [one]
Entering INVOKE_APPLICATION 5
Listener called!
Entering RENDER_RESPONSE 6
Entering RESTORE_VIEW 1
Entering APPLY_REQUEST_VALUES 2
Entering PROCESS_VALIDATIONS 3
Entering UPDATE_MODEL_VALUES 4
Values set: [one, two]
Entering INVOKE_APPLICATION 5
Listener called!
Entering RENDER_RESPONSE 6
Entering RESTORE_VIEW 1
Entering APPLY_REQUEST_VALUES 2
Listener called!
Entering PROCESS_VALIDATIONS 3
Entering UPDATE_MODEL_VALUES 4
Values set: [one, two, three]
Entering INVOKE_APPLICATION 5
Entering RENDER_RESPONSE 6

As you can see, the model values are always set in the UPDATE_MODEL_VALUES phase, while the change event performs in INVOKE_APPLICATION as it should, toggleSelect listener perform in APPLY_REQUEST_VALUES, which is before in the list.

This seems to be a Primefaces bug, which should be notified in their GitHub branch.

See also:

Community
  • 1
  • 1
Aritz
  • 30,971
  • 16
  • 136
  • 217
  • Thanks, I see and understand the behaviour now. However, if one is able to provide the complete list of select options in the bean, it is possible to do a workaround by using the ToggleSelectEvent: `public void toggle(ToggleSelectEvent te) { if(te.isSelected()) { // do sth with this.items }}` ... maybe this is also helpful for s.o. else :) – stg Apr 20 '15 at 15:43
  • There's no problem in using it as long as you perform a whole form submit at the end, cause model refreshes properly. The issue could be if you want to display a message with the selected items in the mean time, as an example. PF doesn't show this behaviour in their showcase, while they do it [in other components like `pickList`](http://www.primefaces.org/showcase/ui/data/pickList.xhtml). – Aritz Apr 20 '15 at 15:54
  • mh? Of course this is a problem ... as mentioned, I want to perform some business logic which depends on the selected values. If the action listener is called prior ro apply_request_values and process_validations, then I am performing this logic with the wrong data. But the provided workaround is fine for me here....however, I agree this is a bug in PF and I am going to report it. Lets see what the PF guys are thinking about it :) – stg Apr 20 '15 at 16:12
  • That must be a bug. I'm already in v5.2 and it keeps happening, so it worths opening an issue for it. And for your workaround it would be nice if you could make an example and publish it as an answer, while PF guys fix it ;-) – Aritz Apr 20 '15 at 16:15
  • 1
    I've checked their changelog and it seems the bug (https://github.com/primefaces/primefaces/issues/318) was fixed with 5.2.4. Unfortunately I am still stuck with PF5.2 – Ben Aug 18 '15 at 13:12
3

The listener gets triggered during the second phase of JSF lifecycle: Apply Request Values. The model (requestBean.list) gets updated later, during the fourth phase, Update Model Values. That's why the listener sees the old value.

BalusC posted a general workaround in this answer, but it will cause an infinite loop in this case because of the way PrimeFaces' components wrap and enqueue events. So I suggest going the phase listener way.

View

<f:view beforePhase="#{requestBean.beforePhase}">
    <h:form id="form">
        <p:selectCheckboxMenu id="list_menu" value="#{requestBean.list}" label="List">
            <f:selectItem itemValue="one" itemLabel="one" />
            <f:selectItem itemValue="two" itemLabel="two" />
            <f:selectItem itemValue="three" itemLabel="three" />
            <!-- still need p:ajax toggleSelect to trigger the request -->
            <p:ajax event="toggleSelect" />
            <p:ajax event="change" listener="#{requestBean.listener}" />
        </p:selectCheckboxMenu>
    </h:form>
</f:view>

Bean

public void beforePhase(PhaseEvent event) {
    if (event.getPhaseId() == PhaseId.INVOKE_APPLICATION) {
        Map<String, String> params = FacesContext.getCurrentInstance()
                .getExternalContext().getRequestParameterMap();
        String eventName = params.get(
                Constants.RequestParams.PARTIAL_BEHAVIOR_EVENT_PARAM);
        String source = params.get("javax.faces.source");
        if ("form-list_menu".equals(source) && "toggleSelect".equals(eventName)) {
            listener();
        }
    }
}
Community
  • 1
  • 1
Vsevolod Golovanov
  • 4,068
  • 3
  • 31
  • 65
0

I stumbled into another workaround: basically you need to populate the label text from within the setter for the value. For example:

View

<p:selectCheckboxMenu 
    value="#{bean.selectedValues}" 
    label="#{bean.selectedValuesDisplayString}"
    widgetVar="checkboxMenuWget">

    <f:selectItems value="#{bean.allValues}"/> 

    <p:ajax event="change" listener="#{bean.updateDisplayString()}" update="@widgetVar(checkboxMenuWget)" />
    <p:ajax event="toggleSelect" update="@widgetVar(checkboxMenuWget)" />
</p:selectCheckboxMenu>

Bean

// Properties
private List<String> allValues;
private List<String> selectedValues;
private String selectedValuesDisplayString;

// Getters and Setters
public void setSelectedValues(List<String> selectedValues) {
    this.selectedValues = selectedValues;
    updateDisplayString();
}

// UI Code
public void updateDisplayString() {
    this.selectedValuesDisplayString = // code to generate display string...
}
0

I also faced this problem. It is still not fixed in PrimeFaces 5.2. My solution how to fix it:

add to faces-config.xml

<component>
    <component-type>org.primefaces.component.SelectCheckboxMenu</component-type>
    <component-class>yourpackage.SelectCheckboxMenu</component-class>
</component>

create this class:

public class SelectCheckboxMenu extends  org.primefaces.component.selectcheckboxmenu.SelectCheckboxMenu {
    @Override
    public void queueEvent(FacesEvent event) {
        FacesContext context = getFacesContext();
        String eventName = context.getExternalContext().getRequestParameterMap().get(Constants.RequestParams.PARTIAL_BEHAVIOR_EVENT_PARAM);

        if(event instanceof AjaxBehaviorEvent && eventName.equals("toggleSelect")) {
            Map<String,String> params = context.getExternalContext().getRequestParameterMap();
            String clientId = this.getClientId(context);
            boolean checked = Boolean.valueOf(params.get(clientId + "_checked"));

            // changed code
            ToggleSelectEvent toggleSelectEvent = new ToggleSelectEvent(this, ((AjaxBehaviorEvent) event).getBehavior(), checked);
            toggleSelectEvent.setPhaseId(event.getPhaseId());
            getParent().queueEvent(toggleSelectEvent);
            // end
        }
        else {
            super.queueEvent(event);
        }
    }

}
0

You can use remote command for listener like following.

<p:remoteCommand name="test" id="test" actionListener"#{requestBean.listener}" update="@form" process="@form"></p:remoteCommand>

Call Following ways above code on toggleselect

        <p:ajax event="toggleSelect" oncomplete="test();" />
-1

Faced the same issue, after searching a while I found that you can bind your update logic to the onchange attribute. Like:

<p:selectCheckboxMenu value="#{requestBean.list}" label="List" onchange="#{requestBean.listener()}">
            <f:selectItem itemValue="one" itemLabel="one" />
            <f:selectItem itemValue="two" itemLabel="two" />
            <f:selectItem itemValue="three" itemLabel="three" />
            <p:ajax event="toggleSelect" listener="#{requestBean.listener}" />
            <p:ajax event="change" listener="#{requestBean.listener}" />
        </p:selectCheckboxMenu>

Consider: onchange="#{requestBean.listener()}" you have to call the listener using braces, otherwise you get an exception.

Don't know which PF version you are using, but I found the solution in this link. (PrimeFaces 5.3)

N.Zukowski
  • 600
  • 1
  • 12
  • 31