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
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:
| Capability | Detail |
|---|---|
| Record context | Loads the record for id request parameter (detail/edit style pages) |
| Field binding | {!Contact.Email}, {!Contact.Account.Name} (relationship traversal within limits) |
| Standard actions | save, quicksave, edit, delete, cancel, and list-oriented actions when using set controllers |
| Security | Respects object/field access in the UI layer when using inputField/outputField patterns |
| Navigation | Many 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(orvoid/nullto 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
| Signal | Why custom controller |
|---|---|
| Wizard spanning multiple objects/steps | Full control of state machine |
| No meaningful single “standard” record | Dashboard-style or utility page |
| You want to avoid extension constructor coupling | Clean slate |
| Heavy non-CRUD logic | Explicit 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 addFieldsis 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
| Need | Prefer |
|---|---|
| Mostly standard record + a little Apex | Extension |
| Page not centered on one standard record | Custom controller |
| Reuse one Apex UI service on many objects | Often 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)
}
| Return | Typical effect |
|---|---|
null | Remain on current page; re-render |
PageReference with redirect | Navigate; often setRedirect(true) to clear view state |
| Standard controller action | return 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)
| Requirement | Standard | Extension | Custom |
|---|---|---|---|
| Edit one Opportunity with standard Save | ✓ | ||
| Edit one Opportunity + custom “Sync to ERP” action | ✓ | possible | |
| Multi-step onboarding wizard creating User + Contact + custom object | ✓ | ||
| Paginated list of Cases with mass update | set controller ± ext | ✓ | ✓ |
| PDF of a Quote using standard record | ✓ (+ maybe ext) | ✓ |
Testing Visualforce Controllers (Overview)
You rarely “click” pages in automated Apex tests. Instead:
- Create test data (
@testSetupor in-test inserts) - Construct the controller:
new MyController()ornew MyExt(new ApexPages.StandardController(record)) - Optionally set
ApexPages.currentPage().getParameters().put('id', record.Id) - Assign properties a user would type; call action methods
- Assert database changes, returned
PageReferenceURLs, and messages - 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.
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?
What is the required constructor signature pattern for a Visualforce controller extension used with a standard controller?
Which Visualforce-related API is specifically designed to manage pagination over a large set of records in list-style pages?