Best Practice: Related Fields with Standard Controller Extensions

So you want to build a StandardController extension, and you’re considering using the StandardController.getRecord() method to get the record supplied by the Standard Controller. The problem is, it doesn’t pull in each of the fields you need to use, right? Now, before you give up and use StandardController.getId() method along with a manually specified list of all of the record’s and parent record fields you need, let’s see what we can do with the getRecord() method.

Consider this code snippet, a simple Controller Extension for Contact:

<apex:page standardController="Contact" extensions="ContactExt">
City: {!city}
</apex:page>

and

public class ContactExt {
    public Contact record {get; set;}
    public String city {
        get {
            return record.Account.BillingCity;
        }
        private set;
    }

    public ContactExt(ApexPages.StandardController sc) {
        record = (Contact)sc.getRecord();
    }
}

Load up /apex/ContactExt?id={some contact id from your org} and you will be greeted by this message:

SObject row was retrieved via SOQL without querying the requested field: Account.BillingCity

Well, how about we utilize the StandardController.addFields(List<String>) method:

public class ContactExt {
    public Contact record {get; set;}
    public String city {
        get {
            return record.Account.BillingCity;
        }
        private set;
    }

    public ContactExt(ApexPages.StandardController sc) {
        sc.addFields(new List<String> {'Account.BillingCity'});
        record = (Contact)sc.getRecord();
    }
}

You could extend it to dynamically list all fields and even parent fields if you wanted. It’s onerous, but it’s still better than manually doing a query using the sc.getId() value. Load up the VF page again, it works! Well, not so fast… try and create a test method. Here’s a sample:

    private static testMethod void testContactExt() {
        Account acct = new Account(Name = 'Test Account');
        insert acct;
        
        Contact contact = new Contact(LastName = 'Test', AccountId = acct.Id);
        insert contact;

        new ContactExt(new ApexPages.StandardController(contact));
    }

You’ll be greeted by this error when you run the test:

System.SObjectException: You cannot call addFields when the data is being passed into the controller by the caller.

There’s a known workaround for this method, which creates an untestable line but solves this issue:

    public ContactExt(ApexPages.StandardController sc) {
        if (!Test.isRunningTest()) {
            sc.addFields(new List<String> {'Account.BillingCity'});
        }
        record = (Contact)sc.getRecord();
    }

By definition, any line inside !Test.isRunningTest() is untestable, and there are a few patterns which require its use — testing callouts comes to mind. This is a workaround, not a good solution by any means.

(Wow, that was quite an introduction. Drumroll please…) There is a better workaround!

You don’t have to include a field in StandardController.addFields() to get it to be included and avoid that pesky SObject row was retrieved via SOQL without querying error. Simply include it in the visualforce page as an <apex:outputField>! Check out this snippet, which resolves the error in question:

<apex:page standardController="Contact" extensions="ContactExt">
<apex:outputField value="{!contact.Account.BillingCity}" />
City: {!city}
</apex:page>
public class ContactExt {
    public Contact record {get; set;}
    public String city {
        get {
            return record.Account.BillingCity;
        }
        private set;
    }

    public ContactExt(ApexPages.StandardController sc) {
        record = (Contact)sc.getRecord();
    }
}

No error! You don’t even need to use the outputField in your page, you can include rendered=”false” — but you do need to have it merge there to have the field pulled into getRecord().

This entry was tagged , , , . Bookmark the permalink.