April 26, 2013

Checking The Boxes, Getting The Input:

Sometimes you expect a problem to have a simple solution, but when it turns out that such an assumption is false, needs dictate that you find a method anyway; usually once that method is found you'll re-use it, again and again, and probably never think to try something else. Times change, and circumstances change, and if you re-visit a problem with an updated set of tools, you may well find a much more graceful solution than the one you used before.

The Problem

Visualforce provides a simple mechanism for displaying a list of checkboxes which are grouped together. Where it starts to struggle is when you break that grouping and are more interested in finding out which selections a user has made, without tying those selections to an SObject field.

For example, if you want to loop over Opportunity records in a page, with a checkbox next to each, and then find out which ones a user has selected it used to be a bit... tricky. was to simply use HTML checkbox input fields, give them all a specific class, and then loop over them with jQuery. I'd put all the selected values (record IDs) into a string, and put that into an apex:inputHiddenThe solution I used many times in the past was to simply use HTML checkbox input fields, give them all a specific class, and then loop over them with jQuery. I'd put all the selected values (record IDs) into a string, and put that into an <apex:inputHidden> field bound to a string property of the controller. Elegant is not the word that springs to mind, and 'cumbersome' seems rather more fitting

The 'New' Solution

Ok, so you could have used this method for quite a while now since we've had dynamic Visualforce bindings since the Spring '11 release... but I'd not needed to do this kind of thing again until now, and when I did encounter it this week I figured there must be a better method than that which I used before. Of course, there is a better method.

Dynamic bindings allow us to bind Visualforce value attributes to collections in our controller, and hence for each row of data we can bind to an index in a list, or to a map. Although the list is workable it requires that the order of data doesn't change, and that filters etc. aren't applied, so maps are the more useful option.

If we're looping over records, then in our controller we'll want to have a map of record ID to Boolean:

public Map<Id, Boolean> selected {get; set;} 
public List<Opportunity> opportunities {get; set;}

and then we populate this map when we load the data for display:

// when data is loaded populate the map 
// don't load data like this :) 
for(Opportunity o : [select Id, Name from Opportunity]) 
{ 
  selected.Put(o.Id, false); 
  opportunities.Add(o); 
}

So far, so good. In our page we can now loop over the opportunities, and in doing so index specific Boolean values in the map using the record IDs:

<apex:pageBlockTable value="{!opportunities}" var="{!o}"> 
  <apex:column> 
    <apex:inputCheckbox value="{!selected[o.Id]}"/> 
  </apex:column> 
  <apex:column> 
    <apex:outputField value="{!o.Name}"/> 
  </apex:column> 
</apex:pageBlockTable>

You can probably guess where we go from here: any action method that needs to find out which records the user has selected need only loop over the map and store the IDs where the mapped Boolean is true. Easy.

Another benefit of this setup is that it's trivial to add a select all / deselect all checkbox in the header facet of the column, as all it needs to do is utilise apex:actionSupport to call an action method that toggles the values:

<apex:pageBlockTable value="{!opportunities}" var="{!o}" id="list"> 
  <apex:column> 
    <apex:facet name="header"> 
      <apex:inputCheckbox value="{!forAll}"> 
        <apex:actionSupport event="onchange" action="{!ToggleAll}" rerender="list"/> 
      </apex:inputCheckbox> 
    </apex:facet> 
    <apex:inputCheckbox value="{!selected[o.Id]}"/> 
  </apex:column> 
  <!-- snip! -->

and the controller code is just as simple:

public Boolean forAll {get; set;} 
// snip! 
public void ToggleAll() 
{ 
  for(Id oppty : selected) 
  { 
    selected.Put(oppty, forAll); 
  } 
}

Other Techniques

Although this is somewhat cleaner than String splitting and the like, I can't shake the feeling that for several years I've overlooked something obvious and made this problem more complicated than it needs to be. I'm sure someone out there has a simpler solution, and if that someone is you then please let me know. Remember, just because you solved a problem before, doesn't mean you found the best solution for it.