11.2 Standard, Custom Controllers & Extensions

Key Takeaways

  • Standard controllers provide record context, field binding, and standard actions (save, edit, delete, cancel, quicksave) for one object type without writing Apex
  • Custom controllers are Apex classes referenced by the controller attribute and fully own data loading, actions, and navigation
  • Controller extensions add Apex to a standard (or custom) controller via a constructor that accepts that controller—use them to extend without rewriting CRUD
  • Getter/setter properties and action methods connect markup expressions to Apex; StandardSetController powers list views, filters, and pagination
  • Test VF controllers with Apex tests that construct pages/controllers, set parameters, call action methods, and assert state—not by clicking the UI alone
Last updated: August 2026

11.2 Standard, Custom Controllers & Extensions

Quick Answer: Use a standard controller when you need one record’s fields and standard actions with little Apex. Use a custom controller when the page’s data and actions do not fit a single standard record lifecycle. Use a controller extension when you want standard controller power plus custom Apex. Bind UI with getters/setters and action methods; use StandardSetController for lists and pagination; unit-test controllers in Apex.

If section 11.1 was the View, this section is the Controller half of Visualforce MVC—and a frequent Platform Developer I decision point.

Standard Controllers

Declare with:

<apex:page standardController="Contact">

What you get without writing Apex:

CapabilityDetail
Record contextLoads the record for id request parameter (detail/edit style pages)
Field binding{!Contact.Email}, {!Contact.Account.Name} (relationship traversal within limits)
Standard actionssave, quicksave, edit, delete, cancel, and list-oriented actions when using set controllers
SecurityRespects object/field access in the UI layer when using inputField/outputField patterns
NavigationMany actions return PageReference results that redirect appropriately

Standard controller for custom objects works the same way: standardController="Invoice__c" and {!Invoice__c.Amount__c}.

When standard controller alone is enough

  • Override a standard button with a VF page that mostly shows/edits the same object
  • Lightweight branding around standard save/cancel
  • Read-only detail with a few related lists via relationships already available

Limits of standard controllers alone

  • Custom multi-object transactions (Account + Contacts + custom children in one save) need Apex
  • Complex SOQL, callouts, or non-CRUD business services need Apex
  • You cannot add arbitrary new properties without an extension or custom controller

Custom Controllers

Declare with:

<apex:page controller="MyInvoiceWizardController">

The Apex class is a normal public class. No special base class is required. Typical responsibilities:

  • Query or construct the data the page needs in the constructor or lazy getters
  • Expose properties for {! } binding
  • Implement action methods returning PageReference (or void / null to stay on page)
  • Enforce sharing (with sharing / without sharing / inherited sharing) deliberately
public with sharing class MyInvoiceWizardController {
    public Invoice__c inv { get; set; }
    public String statusMessage { get; private set; }

    public MyInvoiceWizardController() {
        inv = new Invoice__c(Status__c = 'Draft');
    }

    public PageReference saveInvoice() {
        insert inv;
        PageReference pr = Page.InvoiceThanks;
        pr.setRedirect(true);
        pr.getParameters().put('id', inv.Id);
        return pr;
    }
}

When to choose a custom controller

SignalWhy custom controller
Wizard spanning multiple objects/stepsFull control of state machine
No meaningful single “standard” recordDashboard-style or utility page
You want to avoid extension constructor couplingClean slate
Heavy non-CRUD logicExplicit service API in one class

Trade-off: You re-implement or manually wire anything the standard controller gave you for free (id loading, standard save semantics, automatic record binding root name).

Controller Extensions

Extensions add Apex while keeping a standard (or custom) controller as the primary:

<apex:page standardController="Account" extensions="AccountBalanceExt,AccountMapExt">

Constructor pattern (memorize):

public with sharing class AccountBalanceExt {
    private ApexPages.StandardController std;
    public Decimal balance { get; private set; }

    public AccountBalanceExt(ApexPages.StandardController std) {
        this.std = std;
        Account acct = (Account)std.getRecord();
        // optionally std.addFields(...) when fields missing from page
        balance = computeBalance(acct.Id);
    }

    public PageReference refreshBalance() {
        Account acct = (Account)std.getRecord();
        balance = computeBalance(acct.Id);
        return null; // re-render same page
    }
}

Key facts

  • Constructor must accept ApexPages.StandardController (or the custom controller type if extending a custom controller)
  • Multiple extensions are allowed; order can matter when names collide—avoid duplicate property/action names
  • Call std.save(), std.cancel(), std.view(), getRecord(), getId() to reuse platform behavior
  • addFields is used in extensions/tests when the page did not reference fields you need in Apex before view state is established

When to use an extension (exam favorite)

  • “Use standard Account save and call an external rating service on button click”
  • “Add a custom calculated property beside standard fields”
  • “Override only one behavior but keep standard CRUD”

Extension vs custom controller decision

NeedPrefer
Mostly standard record + a little ApexExtension
Page not centered on one standard recordCustom controller
Reuse one Apex UI service on many objectsOften extension per object or redesign toward LWC + Apex controller class

Getter/Setter Properties

Visualforce binds to Apex properties and getX/setX methods:

public String searchKey { get; set; }

// Equivalent explicit form
private Integer counter = 0;
public Integer getCounter() { return counter; }
public void setCounter(Integer value) { counter = value; }

Markup:

<apex:inputText value="{!searchKey}"/>
<apex:outputText value="{!counter}"/>

Lazy getters that run SOQL are common but dangerous if called multiple times per request—cache in a private variable after first load. Setters run on postback when inputs submit values; understand order: setters populate, then action method runs, then getters for re-render.

transient properties (from 11.1) skip view state—use for large query results you can re-fetch.

Action Methods

Action methods are public Apex methods invoked by commandButton, commandLink, actionSupport, etc.:

public PageReference doSearch() {
    // use searchKey, run SOQL, set results property
    return null; // stay on page
}

public PageReference goHome() {
    return new PageReference('/'); // or Page.SomePage with setRedirect(true)
}
ReturnTypical effect
nullRemain on current page; re-render
PageReference with redirectNavigate; often setRedirect(true) to clear view state
Standard controller actionreturn std.save(); delegates

Use ApexPages.addMessage for user-visible errors; pair with apex:pageMessages.

StandardSetController: Lists, Filters, Pagination

ApexPages.StandardSetController wraps a list of records (from a query locator or list) and provides pagination, selected records, and list-view style behaviors.

public with sharing class ContactListExt {
    public ApexPages.StandardSetController ssc { get; set; }

    public ContactListExt(ApexPages.StandardController unused) {
        // Often used with recordSetVar pages; can also construct from query
    }

    public ContactListController() {
        ssc = new ApexPages.StandardSetController(
            Database.getQueryLocator([
                SELECT Id, Name, Email FROM Contact ORDER BY Name
            ])
        );
        ssc.setPageSize(25);
    }

    public List<Contact> getContacts() {
        return (List<Contact>)ssc.getRecords();
    }

    public void next() { ssc.next(); }
    public void previous() { ssc.previous(); }
}

On the page with a standard list controller:

<apex:page standardController="Contact" recordSetVar="contacts">
  <!-- {!contacts} is the list variable name from recordSetVar -->
</apex:page>

Why exams care: Pagination via StandardSetController is the platform-native answer to “show many records without blowing view state/heap,” paired with getRecords() for the current page only.

Capabilities to recognize: setPageSize, next/previous/first/last, getResultSize, filter/list view integration in standard list contexts, and selected records for mass actions.

Choosing Among the Three (Exam Decision Table)

RequirementStandardExtensionCustom
Edit one Opportunity with standard Save
Edit one Opportunity + custom “Sync to ERP” actionpossible
Multi-step onboarding wizard creating User + Contact + custom object
Paginated list of Cases with mass updateset controller ± ext
PDF of a Quote using standard record✓ (+ maybe ext)

Testing Visualforce Controllers (Overview)

You rarely “click” pages in automated Apex tests. Instead:

  1. Create test data (@testSetup or in-test inserts)
  2. Construct the controller: new MyController() or new MyExt(new ApexPages.StandardController(record))
  3. Optionally set ApexPages.currentPage().getParameters().put('id', record.Id)
  4. Assign properties a user would type; call action methods
  5. Assert database changes, returned PageReference URLs, and messages
  6. For extensions needing fields not on a page in test context, use stdController.addFields(...) or ensure the record query includes fields
@IsTest
static void save_insertsInvoice() {
    MyInvoiceWizardController ctl = new MyInvoiceWizardController();
    ctl.inv.Amount__c = 100;
    PageReference pr = ctl.saveInvoice();
    System.assertNotEquals(null, pr);
    System.assertEquals(1, [SELECT COUNT() FROM Invoice__c]);
}

Coverage note: Testing the Apex class covers controller logic. Markup is not executed line-by-line like Apex; still exercise every action and branch you care about. Governor limits and sharing behavior in tests follow normal Apex test rules (runAs for user context).

Bottom line: Standard controllers = free record CRUD UI. Extensions = standard + custom Apex via StandardController constructor. Custom controllers = full ownership. Properties and actions bind the page; StandardSetController paginates lists; test by constructing controllers and calling methods with assertions.

Test Your Knowledge

A page must use the standard Account save behavior and also expose a custom Apex method that recalculates a non-stored score when the user clicks a button. What is the best controller approach?

A
B
C
D
Test Your Knowledge

What is the required constructor signature pattern for a Visualforce controller extension used with a standard controller?

A
B
C
D
Test Your Knowledge

Which Visualforce-related API is specifically designed to manage pagination over a large set of records in list-style pages?

A
B
C
D