Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

commandButton click in p:dataTable causes @ViewScoped bean to be re@Produced after application invocation

The view:

<h:form ...
  <p:dataTable value="#{myBean.list}" var="data" ...
     <p:column ...
        <h:commandButton action="#{controller.method(data.id)}" />
     </p:column>
  </p:dataTable>
</h:form>

The controller:

@ApplicationScoped
public class Controller {
   public String method(final Long dataId) {
        /* Do business */
        return URL_WITH_REDIRECT;
   }
}

The producer

(using the @ViewScoped CDI annotation as described here)

@ApplicationScoped
public class Producer {
   @Named @ViewScoped @Producer
   public MyBean getMyBean() {
        final MyBean bean = new MyBean();
        bean.list = new ArrayList<Data>(); // where Data has a Long id field
        /* Do business and populate list */
        return bean;
   }
}

The problem & its scenario

  1. GET the page
    1. The bean is produced
    2. View is rendered
    3. Response sent to browser
  2. Click the button
    1. Data is POSTed to server
    2. Phases 1-4 are executed without any issue, and that use @ViewScoped beans as expected
    3. Phase 5: controller.method is called with data.id and accesses beans generated at 1.1
    4. Method returns redirect String
    5. !! The producer is called again !! - we're still in APPLICATION_INVOCATION phase, but after the actual method call
  3. Browser receives redirect
  4. GET next page ...

The half-"donkey" solution that works:

In short: on click, copy the id outside the datatable, and trigger a click on a submit button.

On the h:commandButton inside the table column added :

onclick="$('input[id*=selectedDataId]').val('#{data.id}'); $('button[id*=callMethod]').trigger('click');"

Outside the table:

<h:inputHidden id="{selectedDataId}"binding="#{selectedDataId}"/>
<p:commandButton type="submit"
                 id="callMethod"
                 label="Hidden button"
                 action="#{controller.method(selectedDataId.value)}"/>

At the end it works, but I was not able to figure out what causes the first & base approach to reinitialize the view scoped bean. Looking at the stack trace (see below) it seems like it is rebuilding the rows.

Question:

Does anyone have an explanation, and maybe caveats to look out for regarding this issue?

Stack trace

Where: getPipelinecheckSearchResults is the call for retrieving the list that backs the table, that causes the producer to be called

stack trace

What I've already looked through:

I've read following articles / SO questions without gaining any better understanding on why the above (1st) solution works as it does.

ViewScoped bean is recreated everytime I click on commandButton in my dataTable

Why does @PostConstruct callback fire every time even though bean is @ViewScoped? JSF

How can I pass selected row to commandLink inside dataTable?

http://balusc.blogspot.de/2010/06/benefits-and-pitfalls-of-viewscoped.html

https://java.net/jira/browse/JAVASERVERFACES-1492

like image 542
Matyas Avatar asked Nov 13 '22 04:11

Matyas


1 Answers

I've found some sources for jsf/primefaces/ee-api/glassfish etc. to debug the behavior, so here's the answer:

In short

If a component:

  • Triggers an action (controller.method) that causes redirect
  • And is placed in a datatable
  • And the datatable generates its rows based on a @ViewScoped bean

Then:

  • After the controller.method invocation the @ViewScoped bean that the datatable depends on will be regenerated (with all its dependencies of course)

Tested: In version 2.1.7 of JSF. Looked in sources of 2.1.19, and I'd expect same behavior there.

Details

For those who cry out loud in lonely summer nights asking: "Why?"

The chain of "Events" that lead to this behavior (with references to sources):

  1. The user clicks a button inside a table row.
  2. Data is POSTed to the server
  3. Phases 1-4 go as planned
  4. APPLICATION_INVOCATION
    1. The click event is received by JSF. Important: The click event that references the button is wrapped in an event that contains information about the table & the row number the click happened on. For simplicity: rowEvent & clickEvent
    2. The event is "broadcasted" in the tree of the components @ UIViewRoot:794
    3. javax.faces.UIData The grandparent of org.primefaces.component.datatable.DataTable backing p:datatable starts processing the event @ UIData.broadcast(FacesEvent)
      1. The broadcast method first saves the index of the last selected row
      2. Then it selects the one specified by the rowEvent
      3. Dispatches the clickEvent on the child UIComponent, in our case on the Button
        1. Everything is well & fine, and the event starts getting processed by ActionListener.processAction(ActionEvent)
          1. This in turn invokes controller.method which returns a redirect String and things begin to go downhill
          2. At the end of the method the redirectString is processed by a NavigationHandler
            1. This one seeing that we're about to redirect quickly clears the ViewMap removing all @ViewScoped beans from it at line 179. Which if we think about it is kind of logical, since we're on our way out.
      4. On arriving back in UIData.broadcast which
        • having broadcasted the inner event,
        • not knowing that some inner event caused a redirect and everything it does will be thrown to the garbage (because of 302)
        • as a last action, tries to select the row whose index it saved at step 4.3.1
      5. And of course to select a row, it needs to know the data for it, and this is where the @ViewScoped bean(s) needed by the table get regenerated.

THE END

Notice

Though I haven't tested I'd expect the same behavior h:datatable, p:accordionPanel, p:carousel, p:galleria, p:dataGrid etc. In short every component that subclasses UIData and doesn't provide a redirect - aware broadcast method.

like image 148
Matyas Avatar answered Nov 14 '22 23:11

Matyas