Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PrimeFaces selectCheckboxMenu: Old values in toggleSelect actionListener

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

like image 828
stg Avatar asked Apr 20 '15 11:04

stg


2 Answers

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:

  • How to implement a PhaseListener which runs at end of lifecycle?
like image 138
Xtreme Biker Avatar answered Nov 15 '22 08:11

Xtreme Biker


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();
        }
    }
}
like image 32
Vsevolod Golovanov Avatar answered Nov 15 '22 07:11

Vsevolod Golovanov