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?
Use only a custom controller and reimplement Account save from scratch with no StandardController
Use only standardController="Account" with no Apex, because custom methods are impossible on Visualforce
Use a Batch Apex class as the page controller attribute
Use a controller extension on standardController="Account" that accepts ApexPages.StandardController and adds the recalculation action
What is the required constructor signature pattern for a Visualforce controller extension used with a standard controller?
A no-argument constructor only, same as a custom controller
A constructor that accepts ApexPages.StandardController so the extension can call getRecord(), save(), and related APIs
A constructor that accepts Database.BatchableContext
A static constructor that returns PageReference and cannot take parameters
Which Visualforce-related API is specifically designed to manage pagination over a large set of records in list-style pages?
ApexPages.StandardSetController with setPageSize and next/previous style navigation
Schema.DescribeSObjectResult only
Database.Stateful interface on a trigger handler
System.schedule alone without any query
Sections you finish are checked off in the contents.