Tuesday, 18 June 2013

Selectable SObjects for Use with DataTables in salesforce

Problem

You want to display a list of records in an Apex DataTable, with a checkbox next to each record that allows the user to select one (or many) of the records. You also want to display this type of list for more than one SObject type across your instance in the cleanest way possible, code-wise, without creating a separate Class for each SObject type.

Solution

The general idea of a selectable record is to create a custom Class that contains a SalesForce object of the desired type, and a Boolean value to indicate whether it's selected. The way to do this for a single SObject type could look something like this:
01public class SelectableAsset
02{
03    public Asset asset {get; set;}
04    public Boolean selected {get; set;}
05     
06    public SelectableAsset(Asset ast)
07    {
08        asset = ast;
09        selected = false;
10    }
11}
But, let's say, for example, that in various places within your instance you want to have additional selectable lists of other object types (this example includes Asset, Contact and Line_Item__c). A possible solution could be to create three separate classes with the same form as the class above; one for each of Assets, Contacts and Line_Item__c records. However, using polymorphism, with multiple constructors, and the fact that all objects within SalesForce are a type of SObject, you can condense all of this into one class:
01public with sharing class SelectableSObject
02{
03    private SObject record;
04    public Boolean selected {get; set;}
05     
06        // Universal constructor for any SalesForce object type
07    public SelectableSObject(SObject obj)
08    {
09        record = obj;
10        selected = false;
11    }
12     
13        // Getter for Asset
14    public Asset getAsset()
15    {
16        return (Asset)record;
17    }
18     
19        // Getter for Contact
20    public Contact getContact()
21    {
22        return (Contact)record;
23    }
24     
25        // Getter for Line_Item__c
26    public Line_Item__c getLineItem()
27    {
28        return (Line_Item__c)record;
29    }
30}

Code Explanation

Each constructor takes an argument of a single record of one of the supported object types, which it assigns to the class's private SObject. To retrieve the record, the appropriate getter method is called, which casts the SObject as the desired object type, and returns the result. If you want to include support for additional objects in the future, you can simply add a getter method for each new object type.

Implementation

So far the class only provides a flexible container for a single record with a boolean value attached to indicate whether it's selected. To use these records in a data table Step 1 is to package the records to be displayed into a List of SelectableSObjects. The use case for the example code below is displaying a list of Line_Item__c records for use in adjusting the Line Items attached to an Invoice.
01public List<SelectableSObject> availableLineItems {get; set;};
02 
03...
04 
05this.availableLineItems = new List<SelectableSObject> {};
06 
07// Look up the list of available Line_Item__c records at an Account
08for(Line_Item__c item : [select Id, Name, RecordType.Name, Amount__cInvoice__c,   
09    Date__c from Line_Item__c
10    where Account__c = :this.accountId
11    and (Invoice__c null or Invoice__c = :this.invoiceId)])
12{
13    // Put the Line_Item__c into a SelectableSObject
14    SelectableSObject lineItem = new SelectableSObject(item);
15 
16    // See if this Line Item should start out being selected
17    if(lineItem.getLineItem().Invoice__c == this.invoiceId)
18        lineItem.selected = true;
19 
20    // Add the SelectableSObject to the list
21    this.availableLineItems.add(lineItem);
22}
To display the list of SObjects provided by the controller in a DataTable you would use VisualForce code something like this:
01<apex:pageBlockSection title="Available Line Items" columns="1">
02  <apex:dataTable id="selectLineItemsTable" value="{!availableLineItems}" var="item" width="95%">
03    <apex:column width="5%">
04      <apex:facet name="header"/>
05      <apex:inputCheckBox value="{!item.selected}"/>
06    </apex:column>
07    <apex:column width="25%">
08      <apex:facet name="header">Date</apex:facet>
09      <apex:outputText value="{!item.LineItem.Date__c}"/>
10    </apex:column>
11    <apex:column width="20%">
12      <apex:facet name="header">Amount</apex:facet>
13      <apex:outputText value="{!item.LineItem.Amount}"/>
14    </apex:column>
15    <apex:column width="20%">
16      <apex:facet name="header">Item Type</apex:facet>
17      <apex:outputText value="{!item.LineItem.RecordType.Name}"/>
18    </apex:column>
19    <apex:column width="20%">
20      <apex:facet name="header">Item Name</apex:facet>
21      <apex:outputText value="{!item.LineItem.Name}"/>
22    </apex:column>
23  </apex:dataTable>
24</apex:pageBlockSection>
One handy thing about VisualForce code is that the same short hand that allows you to access a value in a controller that is returned by a method called getValue() by simply referencing 'Value' instead of 'getValue()' applies to the getter methods in the custom class. That's what allows us to access the underlying SObject record cast as a Line_Item__c in each row by just calling 'item.LineItem' instead of 'item.getLineItem()'.
Finally, at the point that you want to capture the selections/de-selections the user has made, you would call a method in the controller or extension that looks through the List of SelectableSObjects, determines which records are checked, and posts updates to records:
01private List<Line_Item__c> lineItemsToUpdate = new List<Line_Item__c> {};
02 
03...
04 
05for(SelectableSObject item : availableLineItems)
06{
07    Line_Item__c liRecord = item.getLineItem();
08 
09    // Determine if the current Line Item is selected and link or clear the  
10    // Invoice__c field
11    if(item.selected)
12        liRecord.Invoice__c this.invoiceId;
13    else
14        liRecord.Invoice__c null;
15 
16    // Add the current line item to a List to be updated
17    lineItemsToUpdate.add(liRecord);
18}
19 
20...
21 
22// Update the set of Line Items
23Database.update(lineItemsToUpdate);

Discussion

Potential Problems

  • The Class contains a single constructor that puts any specifically typed object instance passed into it into a generic SObject instance, then returns the stored instance by casting back into a specific object type, depending on the getter that is called. This creates the possibility to do something like store and Asset record, then request it back as Contact. When I do this the compiler throws back an error once I try to reference a field that is not part of the Asset object, but doesn't return an error about the operation of going from Asset -> SObject -> Contact.
  • The Class needs to be updated to contain a getter method for each object that you want to use it with. It will give you a 'Method does not exist or incorrect signature' error if the getter is missing.

Variations and Improvements

  • If you want the DataTable to allow only one selection instead of many, you would update the apex:inputCheckBox component in the data table to include ActionSupport like this:
    Original
    1<apex:inputCheckBox value="{!item.selected}"/>
    With ActionSupport
    1<apex:inputCheckbox value="{!item.selected}">
    2    <apex:actionSupport event="onclick"
    3        action="{!adjustLineItemSelection}"
    4        rerender="selectLineItemsTable"/>
    5</apex:inputCheckbox>
    IMPORTANT: The controller or extension then needs an additional variable to store the current selection value, and a new method (adjustLineItemSelection in this case), that does the following:
    • 1. If the current selection variable has a value, iterate through the list of selectable items, and set the matching item's selected value to false
    • 2. Iterate through the list a second time to see if there is still another value selected and, if there is, set the current selection variable to that SObject's id value
    There might be a cleaner way to do this using a Map instead of a List.
  • The class could potentially be made more intelligent by including a change flag in addition to the selection flag. This could be used in the Implementation to only save updates to records that have been selected or de-selected instead of the whole List.

References

No comments:

Post a Comment