Keeping track of the selected tab of an apex:tabPanel in the controller

PS Read the next few paragraphs for some context but then move on to the better implementation described in Keeping track of the selected tab of an apex:tabPanel in the controller – using Javascript remoting.

I have a heavily used page that presents a dozen or so fairly complex panels using apex:tab/apex:tabPanel. The controller can set the tab that is initially displayed by providing a property that is used for the “value” (not the “selectedTab”) of the apex:tabPanel. But I also want tab selections made by the user to be recorded back on the controller (and then persisted). This is to improve usability so that the next time the user returns to the page they can continue where they left off. This approach also avoids the need to encode the current tab selection in saveURL and retURL parameters that are part of links to allow child objects to be added or edited and then the same tab returned to when a Save or Cancel is done.

One approach I found suggested is to use a “switchType” of “server” or “ajax” instead of “client”. For my page using either of these settings changed the instantaneous tab switch of the “client” setting to a glacial and feedback-less several second delay in the tab switch. I also found postings that implied that “client” would write the current tab name back to the controller but in my tests it did not.

If you know of a recently tested platform-supported way to do this please comment. Meanwhile here is what I ended up doing.

The problem has two parts:

  • finding which tab is currently selected
  • writing the name of that tab back to the controller

Solving the first problem means inspecting the platform’s implementation classes and so will break if they change. Solving the second problem makes use of published platform functionality and Matt Lacey’s apex:actionFunction and apex:param – Or How to Pass Values from Javascript to an Apex Controller.

Here is an outline of the Visualforce:

<apex:page id="p" standardController="Xyz__c" extensions="XyzController">
    <apex:form id="f">
        <apex:tabPanel id="tp" switchType="client" value="{!selectedTab}" onclick="setSelectedTabOnController()">
            <apex:tab name="Tab1" label="Tab1" id="Tab1">
                <!-- Tab content -->
            </apex:tab>
            <apex:tab name="Tab2" label="Tab2" id="Tab2">
                <!-- Tab content -->
            </apex:tab>
        </apex:tabPanel>
        <apex:actionFunction id="af" name="selectTabActionFunction" action="{!selectTab}" reRender="">
            <apex:param name="selectedTab" assignTo="{!selectedTab}" value="{!selectedTab}"/>
        </apex:actionFunction>
    </apex:form>
<script type="text/javascript">
function getSelectedTabName() {
    if (RichFaces) {
        var tabs = RichFaces.panelTabs['p:f:tp'];
        for (var i = 0; i < tabs.length; i++) {
            var tab = tabs[i];
            if (RichFaces.isTabActive(tab.id + '_lbl')) {
                return tab.name;
            }
        }
    }
    return null;
}
function setSelectedTabOnController() {
    selectTabActionFunction(getSelectedTabName());
}
</script>
</apex:page>

and an outline of the corresponding controller Apex:

public with sharing class XyzController  {
    private Xyz__c xyz;
    // "Get" called when page rendered; "set" called by client-side Javascript
    public String selectedTab {
        get {
            if (selectedTab == null) {
                if (xyz.LastViewedTab__c != null) {
                    // Select the last saved tab
                    selectedTab = xyz.LastViewedTab__c;
                } else {
                    // Select a default tab
                    selectedTab = 'Tab1';
                }
            }
            return selectedTab;
        }
        set {
            if (value != selectedTab) {
                // Persist just the tab change
                upsert new Xyz__c(Id = xyz.Id, LastViewedTab__c = value);
            }
            selectedTab = value;
        }
    }
    // Method for client-side Javascript to invoke; does nothing
    public PageReference selectTab() {
        return null;
    }
    // Other code...
}

Note that the “onclick” event on apex:tabPanel happens after the selected tab has already switched ensuring it is the new tab name that is written to the controller not the old tab name. And the performance of the write to the controller is good enough that the tab switch remains pretty much instantaneous.

Advertisements

Length of “Name” field in “List” setting type custom settings

I’m using a List Custom Setting to allow some attributes used in Apex code to be configured for each picklist entry of a field. So it is natural to make the “Name” of the setting correspond to one of the picklist entries, and then add custom fields to hold the attributes. But a gotcha with this approach is the length of what can be stored in the “Name” field.

First of all the “Name” field reports its length in e.g. the schema browser in Eclipse as 38 characters. But if you hard code that assumption – e.g. truncate any field you are trying to match to 38 characters – you are still at risk of a second problem. That second problem is that the length limit applies to the URL encoded form of the text being entered for the “Name” (confirmed by Salesforce support).

For example this 7 character string:

A/B 'C'
1234567

consumes 13 of the available 38 characters in its URL encoded form:

A%2fB+%27C%27
1234567890123

Note that apart from the length problem this encoding is not visible to code using the custom setting i.e. Apex code always sees the unencoded form.

Some Summer ’12 Apex Highlights

A few things that stood out for me in the Salesforce.com Summer ’12 Release Notes:

  • There is now a Type.newInstance method that allows an object instance to be created starting from a class name string. This mechanism is key to building managed packages that are open for extension by external code: the managed package works to an interface, external code provides a class that implements that interface, and the name of the external code class is set via e.g. a managed package custom setting. See Wanted: the ability to create an instance of an Apex class from the class name for some more background and the comments for the current work-around.
  • It is now possible to run code on package install or upgrade/uninstall by implementing Apex code that implements the InstallHandler/UninstallHandler interfaces. This should allow some manual installation steps to be replaced by automatically run code.
  • There is now support for a Comparable interface providing more freedom in sorting though with the significant penalty that the implementing class needs to be made global.