Saturday 10 May 2014

Transient List Command Button Woes

Doug Grimwade, one of our tech leads at BrightGen, hit a strange problem this week around command buttons (or links) not executing the action method they were associated with, but simply refreshing the page.  Dwindling the page and controller down to their essentials we were left with:

Controller:

public with sharing class RepeatCtrl
{
  public transient List<Account> accounts {get; set;}
  public Integer counter {get; set;}
	
  public RepeatCtrl()
  {
    counter=3;
    setupAccounts();
  }

  private void setupAccounts()
  {
    accounts=[select id, Name from Account order by CreatedDate asc limit :counter];
  }
	
  public void incCounter()
  {
    System.debug('### Inc Counter Called');
    counter++;
    setupAccounts();
  }
}

Page:

<apex:page controller="RepeatCtrl">
  <apex:form >
    <apex:pageBlock title="Accounts">
      <apex:pageblocktable value="{!accounts}" var="acc">
        <apex:column value="{!acc.Name}" />
        <apex:column headerValue="Action">
          <apex:commandButton value="Inc Counter" action="{!incCounter}" />
        </apex:column>
      </apex:pageblocktable>
  
      <br/><apex:outputText value="Counter value = {!counter}" />
    </apex:pageBlock>
  </apex:form>
</apex:page>

 The basic premise of this page (remember this is reduced to reproduce the problem) is to display a list of accounts, with the size of the list dictated by the counter property.  Pressing any of the buttons should increment the counter and refresh the list with an additional element.  

Opening the page displayed the initial list as expected:

Screen Shot 2014 05 10 at 15 35 14

However, clicking a button didn’t work as expected:

Screen Shot 2014 05 10 at 15 36 21

the list has disappeared and the counter value hasn’t incremented.  Investigations in the log file showed that the action method wasn’t being called, as the debug information didn’t appear.  

Moving the post back to an action function, invoked by a click handler on the command button fixed things:

<apex:actionFunction action="{!incCounter}" name="incCounter"/>
...
<apex:commandButton value="Inc Counter" action="{!incCounter}"
        	onclick="incCounter(); return false;"/>

 

As did adding a command button outside the pageblocktable worked correctly - the counter was incremented and the list displayed four elements:

Screen Shot 2014 05 10 at 15 39 38

So it appeared that the issue, for some reason, was that the component carrying out the postback was nested inside the pageblocktable iterator element.  

While I dug around on the Developer Forums for a solution, Doug was trying various tweaks to the code and found the problem - the transient nature of the list of accounts.  Making this list non-transient and storing it in the Visualforce view state fixed the problem: 

public with sharing class RepeatCtrl
{
	public List<Account> accounts {get; set;}

...

Armed with this information I continued googling to see if this was documented behaviour, but this turned up no results.  Remembering that Visualforce is essentially built on JSF, I searched in that direction, and turned up a promising hit:

JSF2 Command Button Not Working

which states:

If command button is nested in a ui:repeat tag, command button or link will not work until the iterating bean/List is in view scope or bigger scope.

...

JSF iteration controls generally maintain a cursor to do their work, and the only way that the cursor can function properly is if the referenced datamodel is in a non-transient scope. For JSF1, that means Session Scope or higher. For JSF2, View Scope or higher.

View Scope in JSF equates to View State in Visualforce, so this matched our scenario.  Even though the command button didn’t use or rely on any element in the list being iterated, that fact that it was inside the iterator meant that the list had to be reconstructed server side before the command button would work correctly.  

A couple of lessons learned here then - transient has some side effects and if you can’t find behaviour documented in Visualforce, its always worth a look at the building blocks of the technology. We were quite pleased that it didn’t take us 6 hours to find the fix, even if we didn’t understand the solution correctly, unlike the poster with the JSF problem. Hopefully this blog will help the next developer to encounter this issue fix it even faster.

One last thing on an unrelated note - the @forcedotcom twitter handle has been replaced with @salesforcedevs 

2 comments:

  1. I ran into same problem few days back, wish I had this post. After wasting few hours, I ended up doing something similar i.e. moving Apex binded function to ActionFunction and calling it via a normal button. I didn't used commandButton to avoid any possible side effects. Made normal button look like salesforce via
    <button class="btn" onclick="myActionFunction();" >

    ReplyDelete
  2. Bob -- as usual, brilliant!

    ReplyDelete