Search code examples
ajaxjsfuirepeat

ui:repeat value expression evaluated on every ajax request


I have a page with a very simple form which submits ajax requests targeted only at components in that same form. In the same page (but outside the form), there is also a ui:repeat which iterates over an array returned from a request scoped managed bean (suppose a list of product categories). This bean has no properties bound in the form and is not accessed in any other way apart from the value attribute of the ui:repeat tag. I don't understand why JSF needs to recreate the request scoped bean on every ajax postback, just as if I asked to render this external ui:repeat (which has nothing to do with the form) along with some component inside the form.

Is it a bug? Or is it an expected behavior? Of course I could annotate the bean as ViewScoped but I don't see the reason to have categories stored in the view scope as they are completely static between postbacks.

Another solution/workaround I found is to render the ui:repeat only in case of non-ajax requests:

<ul>
    <ui:repeat value="#{someRequestScopedBean.categories}" var="category" rendered="#{not facesContext.partialViewContext.ajaxRequest}">
        <li>#{category.name}</li>
    </ui:repeat>
</ul>

but I don't know if this could cause problems and looks not very clear.

TEST CASE

index.xhtml:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:f="http://xmlns.jcp.org/jsf/core">
    <h:head>
        <title>Facelet Title</title>
    </h:head>
    <h:body>
        <h1>Test page</h1>
        <p>Random jokes</p>
        <ul>
            <ui:repeat value="#{oneLiners.list}" var="oneliner">
                <li>#{oneliner}</li>
            </ui:repeat>
        </ul>
        <h:form>
            <h:selectOneMenu value="#{backingBean.greeting}" hideNoSelectionOption="true">
                <f:selectItem value="#{null}" itemLabel="Select a greeting..." noSelectionOption="true"/>
                <f:selectItems value="#{backingBean.greetings}"/>
                <f:ajax render="@this btn"/>
            </h:selectOneMenu>
            <h:commandButton id="btn" value="Say Hello!" disabled="#{empty backingBean.greeting}">
                <f:ajax render="otxt"/>
            </h:commandButton>
            <h:outputText id="otxt" value="#{backingBean.greeting}, Maurizio!" style="display: #{empty backingBean.greeting ? 'none' : 'block'}"/>
        </h:form>
    </h:body>
</html>

Request scoped bean:

package testuirepajax;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;

/**
 *
 * @author maurizio
 */
@ManagedBean
@RequestScoped
public class OneLiners {
    private String[] list;

    public OneLiners() {
        System.out.println("testuirepajax.OneLiners.<init>()");
        list = new String[] {
            "Life is wonderful. Without it we'd all be dead.",
            "Daddy, why doesn't this magnet pick up this floppy disk?",
            "Daddy, what does FORMATTING DRIVE C mean?",
            "Never forget: 2 + 2 = 5 for extremely large values of 2.",
            "C:\\ is the root of all directories."
        };
    }

    public String[] getList() {
        System.out.println("testuirepajax.OneLiners.getList()");
        return list;
    }
}

Form backing bean:

package testuirepajax;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;

/**
 *
 * @author maurizio
 */
@ManagedBean
@ViewScoped
public class BackingBean {

    private String[] greetings;
    private String greeting;

    public BackingBean() {
        System.out.println("testuirepajax.BackingBean.<init>()");
        greetings = new String[] {
          "Hello", "Hi", "Good morning", "Good evening", "Good night"
        };
    }

    public String[] getGreetings() {
        return greetings;
    }

    public void setGreeting(String greeting) {
        this.greeting = greeting;
    }

    public String getGreeting() {
        return greeting;
    }
}

Check the output of your container. Using the Payara Server (which ships Mojarra 2.2.12), I see lines like these:

Informazioni:   testuirepajax.OneLiners.<init>()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.<init>()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()

when selecting elements from the menu or clicking the "Say Hello!" button.


Solution

  • I placed a breakpoint on getList() method and inspected the call stack when it's "unnecessarily" hit during postback in order to learn about the who and the why:

    Daemon Thread [http-nio-8088-exec-5] (Suspended (breakpoint at line 23 in OneLiners))   
        owns: NioChannel  (id=83)   
        OneLiners.getList() line: 23    
        NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]  
        NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62  
        DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43  
        Method.invoke(Object, Object...) line: 497  
        BeanELResolver.getValue(ELContext, Object, Object) line: 97 
        DemuxCompositeELResolver._getValue(int, ELResolver[], ELContext, Object, Object) line: 176  
        DemuxCompositeELResolver.getValue(ELContext, Object, Object) line: 203  
        AstValue.getValue(EvaluationContext) line: 169  
        ValueExpressionImpl.getValue(ELContext) line: 184   
        TagValueExpression.getValue(ELContext) line: 109    
        UIRepeat.getValue() line: 279   
        UIRepeat.getDataModel() line: 255   
        UIRepeat.visitTree(VisitContext, VisitCallback) line: 727   
        HtmlBody(UIComponent).visitTree(VisitContext, VisitCallback) line: 1700 
        UIViewRoot(UIComponent).visitTree(VisitContext, VisitCallback) line: 1700   
        PartialViewContextImpl.processComponents(UIComponent, PhaseId, Collection<String>, FacesContext) line: 403  
        PartialViewContextImpl.processPartial(PhaseId) line: 266    
        UIViewRoot.processDecodes(FacesContext) line: 927   
        ApplyRequestValuesPhase.execute(FacesContext) line: 78  
        ApplyRequestValuesPhase(Phase).doPhase(FacesContext, Lifecycle, ListIterator<PhaseListener>) line: 101  
        LifecycleImpl.execute(FacesContext) line: 198   
        FacesServlet.service(ServletRequest, ServletResponse) line: 658 
        ApplicationFilterChain.internalDoFilter(ServletRequest, ServletResponse) line: 291  
        ApplicationFilterChain.doFilter(ServletRequest, ServletResponse) line: 206  
        WsFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 52    
        ApplicationFilterChain.internalDoFilter(ServletRequest, ServletResponse) line: 239  
        ApplicationFilterChain.doFilter(ServletRequest, ServletResponse) line: 206  
        StandardWrapperValve.invoke(Request, Response) line: 212    
        StandardContextValve.invoke(Request, Response) line: 106    
        FormAuthenticator(AuthenticatorBase).invoke(Request, Response) line: 502    
        StandardHostValve.invoke(Request, Response) line: 141   
        ErrorReportValve.invoke(Request, Response) line: 79 
        AccessLogValve(AbstractAccessLogValve).invoke(Request, Response) line: 616  
        StandardEngineValve.invoke(Request, Response) line: 88  
        CoyoteAdapter.service(Request, Response) line: 521  
        Http11NioProcessor(AbstractHttp11Processor<S>).process(SocketWrapper<S>) line: 1096 
        Http11NioProtocol$Http11ConnectionHandler(AbstractProtocol$AbstractConnectionHandler<S,P>).process(SocketWrapper<S>, SocketStatus) line: 674    
        NioEndpoint$SocketProcessor.doRun() line: 1500  
        NioEndpoint$SocketProcessor.run() line: 1456    
        ThreadPoolExecutor(ThreadPoolExecutor).runWorker(ThreadPoolExecutor$Worker) line: 1142  
        ThreadPoolExecutor$Worker.run() line: 617   
        TaskThread$WrappingRunnable.run() line: 61  
        TaskThread(Thread).run() line: 745  
    

    The interesting lines are the ones above FacesServlet. The class/method names already kind of speak for themselves.

    It thus happened during apply request values phase, when the partial request needs to process the decode of components. The component tree is visited in order to find the components identified by client IDs specified in <f:ajax execute> (which defaults to @this). As the <ui:repeat> is in the way before the component of interest, it's inspected first. A visitTree() triggers full iteration because the client IDs of interest are only available during iteration.

    When I moved <ui:repeat> to below the <h:form>, it isn't invoked anymore. All components of interest have already been found at that point.

    This behavior is, unfortunately, "by design". Your work around is a good one. Better is to check #{not facesContext.postback} instead as this also covers non-ajax postbacks.