More on sorting a custom table

This post describes a mild re-working of http://wiki.developerforce.com/index.php/Sorting_Tables; the real solution here is platform support so please vote as described in that article.

(PS See my comment below about using the jQuery tablesorter plugin – probably a better way to go than this server-side solution.)

The reason to make changes is that I wanted to show the current sort direction using the same arrows that the default UI uses and provide a tooltip to let the user know what direction they will get when they click a header. This in turn required more properties to be exposed per column in the Visualforce. And so to control the complexity of that (and avoid hard to find bugs and simplify future refactoring) some changes were needed.

Here is an extract from the finished custom page. All these extracts just show one table column and … for other markup or code that has been taken out. The key point here is the simplicity of the c:SortableLink line.

<style type="text/css">
/* These standard classes tweaked to position the up and down arrow images correctly */
body .pbBody .sortAsc {
    padding-right: 12px;
    background-position-x: right;
    background-position-y: 4px;
}
body .pbBody .sortDesc {
    padding-right: 12px;
    background-position-x: right;
    background-position-y: -8px;
}
<style>
...
<apex:pageBlockTable value="{!payments}" var="row"  id="paymentsTable">
    ...
    <apex:column value="{!row.Status__c}">
        <apex:facet name="header">
            <c:SortableLink sortable="{!statusSortable}" rerender="paymentsTable"/>
        </apex:facet>
    </apex:column>
    ...
</apex:pageBlockTable>

Most of the markup has been moved into this component:

// SortableLink.component
<apex:component >

    <apex:attribute name="sortable" Type="Sortable" required="true" description=""/>
    <apex:attribute name="rerender" Type="Id" required="true" description=""/>

    <apex:commandLink value="{!sortable.label}"
            action="{!sortable.sort}"
            styleClass="{!sortable.styleClass}"
            title="{!sortable.tooltip}"
            rerender="{!rerender}"
            onclick="this.disabled=true; this.innerHTML='Sorting...';"
            />
</apex:component>

That component is relying on the necessary information being held by an instance of this simple class for each column in the controller:

/**
 * Provide sort-related data for a particular field to simplify the creation of a sort direction
 * table header link in a custom sortable table.
 *
 * Delegates to the underlying controller to get the work done.
 */
public class Sortable {

    /**
     * Underlying controller should implement this.
     */
    public interface Sorter {
        PageReference sort(String fieldName);
        Boolean isAscending(String fieldName);
        Boolean isDescending(String fieldName);
        String getLabel(String fieldName);
    }
    
    private String fieldName;
    private Sorter sorter;

    public Sortable(String fieldName, Sorter sorter) {
        this.fieldName = fieldName;
        this.sorter = sorter;
    }
    
    public String label {
        get {
            return sorter.getLabel(fieldName);
        }
    }
    
    public String tooltip {
        get {
           if (sorter.isAscending(fieldName)) {
               return 'Ascending; click for descending';
           } else if (sorter.isDescending(fieldName)) {
               return 'Descending; click for ascending';
           } else {
               return 'Click for ascending';
           }
        }
    }

    public String styleClass {
        get {
           if (sorter.isAscending(fieldName)) {
               return 'sortAsc';
           } else if (sorter.isDescending(fieldName)) {
               return 'sortDesc';
           } else {
               return '';
           }
        }
    }
    
    public PageReference sort() {
        return sorter.sort(fieldName);
    }
}

So the controller has to have a property (unless I’ve missed something in the Visualforce expression language and you can pass parameters to methods) that is an instance of that class per column and also implement the Sortable.Sorter interface:

public class PaymentController implements Sortable.Sorter {

    // Cached as subject to governor limits
    private static Map FIELDS = Schema.SObjectType.Payment__c.fields.getMap();

    // Meta data to simplify the markup in the page for sorting
    ...
    public Sortable statusSortable { get; set; }
    ...

    public PaymentCalculationController(ApexPages.StandardController controller) {
        ...
        statusSortable = new Sortable('Status__c', this);
        ...
    }
    
    private String paymentsAscField;
    private String paymentsDescField;
    
    public PageReference sort(String fieldName) {
        if (fieldName == paymentsAscField) {
            paymentsAscField = null;
            paymentsDescField = fieldName;
        } else {
            paymentsDescField = null;
            paymentsAscField = fieldName;
        }
        applyCurrentSort();
        return null;
    }
    
    public Boolean isAscending(String fieldName) {
        return fieldName == paymentsAscField;
    }
    
    public Boolean isDescending(String fieldName) {
        return fieldName == paymentsDescField;
    }
    
    public String getLabel(String fieldName) {
        Schema.DescribeFieldResult f = FIELDS.get(fieldName).getDescribe();
        return f.label;
    }

    private void applyCurrentSort() {
        Boolean ascending;
        String fieldName;
        if (paymentsAscField != null) {
            ascending = true;
            fieldName = paymentsAscField;
        } else if (paymentsDescField  != null) {
            ascending = false;
            fieldName = paymentsDescField;
        } else {
            // Default to this
            ascending = true;
            fieldName = 'PayeeName__c';
            paymentsAscField  = fieldName;
        }
        payments = (List) SortUtil.sort(payments, fieldName, ascending, null);
    }

    // Collection shown in table
    public List payments {
        get {
            if (payments == null) {
                payments = [select ...];
                applyCurrentSort();
            }
            return payments;
        }
        set;
    }

    ...
}

I ended up using a simplified version of “SuperSort” to do the work:

/**
 * Refactored from http://wiki.developerforce.com/index.php/Sorting_Tables.
 */
public class SortUtil {

    /**
     * This provides in-memory sorting for cases where it isn't appropriate to re-execute a query
     * with a changed "order by".
     *
     * The fieldValueMapping argument allows e.g. a reference id to be translated to the natural
     * value (e.g. the object name) for use in the sort.
     *
     * The returned list can be downcast to the same SObject type as the argument list.
     */
    public static List sort(
            List items,
            String fieldName,
            Boolean ascending,
            Map fieldValueMapping
            ) {
    
        // Field value to the List of SObjects that have that field value
        Map<Object, List> m = new Map<Object, List>();
        for(SObject item : items){
            Object fieldValue = item.get(fieldName);
            if (fieldValueMapping != null && fieldValueMapping.containsKey(fieldValue)) {
                fieldValue = fieldValueMapping.get(fieldValue);
            }
            List l = m.get(fieldValue);
            if (l == null) {
                l = new List();
                m.put(fieldValue, l); 
            }
            l.add(item);
        }
        
        // Sort the field values
        List fieldValues = new List(m.keySet());
        fieldValues.sort();
        
        // Doing this to end up with a list of the right type that can be downcast
        List results = items.clone();
        results.clear();
        
        // Order the SObjects
        if (ascending) {
            for (Integer i = 0; i < fieldValues.size(); i++) {
                Object fieldValue = fieldValues.get(i);
                List<SObject> l = m.get(fieldValue);
                for (Integer j = 0; j < l.size(); j++) {
                    results.add(l.get(j));
                }
            }
        } else {
            for (Integer i = fieldValues.size() - 1; i >= 0; i--) {
                Object fieldValue = fieldValues.get(i);
                List<SObject> l = m.get(fieldValue);
                // If there is some natural ordering here reverse that too
                for (Integer j = l.size() - 1; j >= 0; j--) {
                    results.add(l.get(j));
                }
            }
        }

        return results;
    }
}

All in all an awful lot of work to achieve something that the platform could do way better.

Advertisements

3 thoughts on “More on sorting a custom table

  1. This is a totally awesome tip, but when I implement, I get the arrow displayed in all of my column headers. Can you point out where in your code the arrows are constrained to just the column header that is clicked?

  2. Sure, when Sorter.styleClass returns the empty string there will be no sort arrows (as these are done through CSS styling) and this relies on the implementation of Sortable.Sorter.isAscending and Sortable.Sorter.isDescending returning false from all but the last sorted column which the sample implementation above does as only one of paymentsAscField or paymentsDescField is ever set (to the name of one of the columns).

    Hope that helps.

  3. PS I just implemented a page doing all the sorting at the client-side using this jQuery plugin http://tablesorter.com/docs/ and the result is way cleaner code-wise – just a few lines of Javascript and some CSS – and snappier for the user as they don’t have to wait for a server request/response cycle. So I recommend using this client-side approach: google blogs on the subject – I didn’t find the perfect post and had to collect bits and pieces from different posts.

    The only thing I had a little trouble with was getting the header columns to display the up/down arrows. I ended up with this:

    <style type="text/css">
    /* Force.com uses backround-image for shaded blue header; replace that styling with this */
    table.tablesorter thead tr .header {
        background-image: url({! URLFOR($Resource.jQueryZip, 'bg.gif') }) !important;
        background-repeat: no-repeat !important;
        background-position: center right !important;
        cursor: pointer !important;
        background-color: #d0eef8 !important;
    }
    table.tablesorter thead tr .headerSortUp {
        background-image: url({! URLFOR($Resource.jQueryZip, 'asc.gif') }) !important;
    }
    table.tablesorter thead tr .headerSortDown {
        background-image: url({! URLFOR($Resource.jQueryZip, 'desc.gif') }) !important;
    }
    </style> 
    

    and just this Javascript:

    <!-- Sorting done by this jQuery plugin -->
    <script type="text/javascript" language="javascript" src="{!URLFOR($Resource.jQueryZip, 'jquery.js')}"></script>
    <script type="text/javascript" language="javascript" src="{!URLFOR($Resource.jQueryZip, 'jquery-tablesorter.js')}"></script>
    <script type="text/javascript" language="javascript">
    var j$ = jQuery.noConflict();
    j$("table").tablesorter({
        sortList: [[0, 0], [5, 1]]
    });
    </script>
    

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s