Saturday, 5 March 2011

Visualforce Dynamic Map Bindings

A new feature in Spring 11 is Dynamic Visualforce Bindings.  Most of the documentation on this is around determining the record fields to display at runtime rather than compile time.  There's also a small section about using this for Lists and Maps.  Lists have been usable in a variety of ways such as apex:repeat and apex:pageBlockTable tags, but being able to retrieve the value from a map based on a key in a Visualforce page is something I've been working around for a while.  The workarounds haven't been unwieldy or difficult, but they've always been a compromise.

I've created a small example to demonstrate the usefulness of this new feature.  This is a page that displays either all accounts in the system:



 or just those that begin with a particular letter,



depending on the option that the user selects from a list.  Prior to Spring 11, I'd have to generate the list of accounts based on the user's selection server side.  As I said earlier, not a lot of work but I'd still prefer not to have to do it.

With the new feature, I can simply set up all my data in a map when constructing the page controller and then dynamically render the appropriate value from the map, based on the key, which is the user's selection.

The page markup is shown below:

<apex:page controller="DynamicBindingsMapExample">
  <apex:form >
    <apex:actionFunction name="redraw_accounts" rerender="accs" status="status"/>
    <apex:pageBlock title="Criteria">
       <apex:outputLabel value="Starting Letter"/>
       <apex:selectList value="{!selectedKey}" size="1" onchange="redraw_accounts()">
          <apex:selectOptions value="{!keys}" />
       </apex:selectList>
    </apex:pageBlock>
    <apex:pageBlock title="Accounts">
       <apex:actionstatus id="status">
          <apex:facet name="start"/>
          <apex:facet name="stop">
             <apex:outputPanel id="accs">
                <apex:pageBlockTable value="{!accountsMap[selectedKey]}" var="acc">
                   <apex:column value="{!acc.name}"/>
                   <apex:column value="{!acc.BillingStreet}"/>
                   <apex:column value="{!acc.BillingCity}"/>
                   <apex:column value="{!acc.BillingPostalCode}"/>
                </apex:pageBlockTable>
             </apex:outputPanel>
          </apex:facet>
       </apex:actionstatus>
    </apex:pageBlock>
  </apex:form>
</apex:page>


The accounts are made available to the page via a map property on the controller that associates a letter (or the String 'All') with a list of Accounts starting with that letter (or every account in the case of 'All').

public Map<String, List<Account>> accountsMap {get; set;}

The user's selection is stored in the selectedKey controller property and this is used to extract the appropriate accounts from the map in the pageblocktable component:

<apex:pageBlockTable value="{!accountsMap[selectedKey]}" var="acc">

When the user's selection changes, the redraw actionfunction is executed to redraw the pageBlockTable containing the accounts. Note that there is no action attribute specified, as I haven't had to write any server side code to change the accounts displayed - everything is handled client side.

<apex:actionFunction name="redraw_accounts" rerender="accs" status="status"/>



Update 12/02/2012 - as requested by Raj in the comments, here is the controller class:

public class DynamicBindingsMapExample 
{
    public Map> accountsMap {get; set;}
    public List keys {get; set;}
    public String selectedKey {get; set;}
    public Map accsByName {get; set;}
    
    public Set getMapKeys()
    {
    	return accountsMap.keySet();
    }
    
    public DynamicBindingsMapExample()
    {
    	accsByName=new Map();
    	List sortedKeys=new List();
    	accountsMap=new Map>();
    	accountsMap.put('All', new List());
    	List accs=[select id, Name, BillingStreet, BillingCity, BillingPostalCode 
    	                    from Account
    	                    order by Name asc];
    	                    
    	                    
    	for (Account acc : accs)
    	{
    		accountsMap.get('All').add(acc);
    		String start=acc.Name.substring(0,1);
    		List accsFromMap=accountsMap.get(start);
    		if (null==accsFromMap)
    		{
    			accsFromMap=new List();
    			accountsMap.put(start, accsFromMap);
    		}
    		accsFromMap.add(acc);
    		accsByName.put(acc.name, acc);
    	}
    	
    	keys=new List();
    	for (String key : accountsMap.keySet())
    	{
    		if (key != 'All')
    		{
    			sortedKeys.add(key);
    		}
    	}
    	sortedKeys.sort();
    	sortedKeys.add(0, 'All');
    	
    	for (String key : sortedKeys)
    	{
    		keys.add(new SelectOption(key, key));
    	}
    	
    	selectedKey='All';
    }
}

5 comments:

  1. Nice post. I found that I ran into an issue trying to use tags within the context of a list of sobjects referenced as a map value. It looks like your use of "value" in the columns of your table above didn't have the same effect, which is good news.

    I look forward to being able to use this approach with input fields. Until then, I'm still making custom wrappers.

    ReplyDelete
  2. Great post! I had no idea we could use the fieldset notation in this fashion, I'd thought it could only be used to reference a field in an sobject.

    ReplyDelete
  3. Hey bob
    Could you please provide controller for this since i started to read your blog recently.I will understand better if you provide controller code.

    ReplyDelete
  4. Hi Raj,

    I've updated the post to include the controller code.

    ReplyDelete
  5. Error: DynamicBindingsMapExample Compile Error: expecting a right angle bracket, found '=' at line 3 column 36

    ReplyDelete