Sunday, December 4, 2011

Why won't this damn thing reRender properly...

Since Visualforce came into being, the aspect of it I've had the most trouble with is the reRender, or more specifically, getting reRenders to behave the way I want them to.  This is partly because much of the coding I did before Salesforce was behind the scenes logic and not web interface design, but it's partly because reRenders can be a real pain in the ass.  Along the way, I've figured out some tips and thought I'd share.

actionRegion
You will drive yourself insane with reRenders if you don't know how to use actionRegions. actionRegions are to submitting what reRenders are to rerendering.  Perhaps a little more explanation and a demo are in order.

When you perform any action on your VF page, by default the entire form is submitted.  You may not actually need everything submitted though; you may just be need a portion of your variables submitted, or maybe none at all.  Submitting everything needlessly means that not only are you slowing down performance of your page, but the page is attempting to validate what's being submitted before it sends anything back to the controller.  So if you have any required inputFields, for instance, those fields will need to be filled in before the form submits at all.  And if you don't know that's what's happening, either because your form has no <apex:pageMessages/> or it's not being reRendered, it will appear as if your action isn't working for no reason at all. 

Let's use this simple page and controller as an example, first without an actionRegion (incorrect):
Page before adding actionRegion (the wrong way)
<apex:page standardController="Account" extensions="extTest">
    <apex:form >
        <apex:pageBlock>
            <apex:pageBlockButtons >
                <apex:commandButton value="Show Billing Street" reRender="opBilling">
                    <apex:param value="true" assignTo="{!ShowBilling}"/>
                </apex:commandButton>
            </apex:pageBlockButtons>
           
            <apex:pageBlockSection >
                <apex:inputField value="{!Account.Name}"/>
            </apex:pageBlockSection>
           
            <apex:outputPanel id="opBilling">
                <apex:pageBlockSection rendered="{!ShowBilling}" id="pbMailing">
                    <apex:inputField value="{!Account.BillingStreet}"/>
                </apex:pageBlockSection>
            </apex:outputPanel>
        </apex:pageBlock>
    </apex:form>
</apex:page>
 Extension:
public with sharing class extTest {
    public Boolean ShowBilling {get;set;}
   
    public extTest(ApexPages.StandardController controller) {
        ShowBilling = false;
    }

}
The idea is, the page opens showing only the Account Name field. Clicking the Showing Billing Street button will update the ShowingBilling variable to true and reRender the opBilling outpanel so the Billing Street field is displayed.  As it's written above however, it will not work if the Account Name field isn't filled in first.  That Name field is required, so the command button tries to submit the whole form and it fails validation without a Name value entered.  Since only the opBilling outputPanel is being reRendered, you won't event see an error message since pageMessages isn't being reRendered (Debug logs are your friend when troubleshooting such issues).

One way to fix this is by updating the page with an actionRegion around the commandButton like so:
pageBlockButtons after adding actionRegion (one right way)
<apex:pageBlockButtons >
                <apex:actionRegion >
                    <apex:commandButton value="Show Billing Street" reRender="opBilling">
                        <apex:param value="true" assignTo="{!ShowBilling}"/>
                    </apex:commandButton>
                </apex:actionRegion>
 </apex:pageBlockButtons>
Now with actionRegion around that button, only the values inside of the actionRegion are submitted, so BillingStreet will be rendered with or without a value in the Account Name field.

Immediate
For the sake of keeping the example above simple, the button didn't do anything other than updating ShowBilling using the param tag and reRendering opBilling.  In reality, you'd only need actionRegion when you need some portion of the variables on the page submitted to the controller/extension to perform logic in a method.  If you don't need to submit any variables, then you can use the "immediate" property of the tag that's performing an action.  Let's see this with the example above:
pageBlockButtons after adding immediate (another right way)
<apex:pageBlockButtons >
                <apex:commandButton value="Show Billing Street" immediate="true" reRender="opBilling">
                    <apex:param value="true" assignTo="{!ShowBilling}"/>
                </apex:commandButton>
</apex:pageBlockButtons>
Make sure you know what immediate means before using it. I've seen people set immediate to true to avoid issues with required fields not realizing that their controller/extension was not receiving any updated variable values from their page, thereby screwing up their logic and leaving them flummoxed.

Choosing what to reRender
I'll probably expand on this in a separate post because there are a lot of scenarios to consider, but I'll use the example above to give one basic tip. 
You may have noticed in my example that I've surrounded the pageBlockSection that contains BillingStreet with an outputPanel, and the reRender acts on that outputPanel.  This may seem like an extra component- why not just reRender the pageBlockSection itself and skip that outputPanel altogether?

What I've learned is that if a reRender updates a variable on your page, you need to reRender a parent of the tag that contains that variable, and not the tag itself. OutputPanels are a useful means of doing so.  To see this in action, try updating the example above so the button attempts to reRender the pageBlockSection directly:
pageBlockButtons with reRender of the pageBlockSection (wrong way)
<apex:pageBlockButtons >
                <apex:commandButton value="Show Billing Street" immediate="true" reRender="pbMailing">
                    <apex:param value="true" assignTo="{!ShowBilling}"/>
                </apex:commandButton>
  </apex:pageBlockButtons>
If you try this example with the commandButton set to reRender the pageBlockSection, it just doesn't work.  So either you need that outputPanel surrounding the pageBlockSection, or alternatively you could move the rendered="ShowBilling" property to the inputField instead, but in this example, that would mean you're rendering an empty pageBlockSection.

As above, there are more scenarios to explore here, but I'll leave it at this for now and hopefully this is helpful to people frustrated with the nuances of reRendering.

1 comment: