Study guide
11.2 Standard, Custom Controllers & Extensions
Salesforce Certified Platform Developer I Exam
Track your progress as you read
Finished sections get a check mark.
1 Introduction & Exam Overview
1.1 Current Exam Facts 1.2 Blueprint Weights & Study Strategy
2 Platform Fundamentals
2.1 Multi-Tenant Architecture & Shared Resources 2.2 MVC Mapping on the Lightning Platform 2.3 Declarative vs Programmatic Decisions 2.4 Agentforce for Developers: Use Cases & Limitations
3 Data Modeling & Schema
3.1 Objects, Fields & Schema Design 3.2 Relationship Types & Implications 3.3 Schema Design Impact on Apex 3.4 Data Import & Export for Development
4 Platform Data Features
4.1 Formula Fields & Roll-Up Summaries 4.2 Core CRM Objects & Standard Schema 4.3 AppExchange Extensions & Platform Boundaries
5 Declarative Process Automation
5.1 Record-Triggered Flows & Declarative Automation 5.2 Approval Processes & Business Rules 5.3 When to Use Apex vs Declarative Tools
6 Apex Language Basics
6.1 Variables, Constants & Apex Data Types 6.2 Lists, Sets & Maps 6.3 Control Flow & Exception Handling 6.4 Classes, Interfaces & OOP Patterns
7 SOQL, SOSL & DML
7.1 SOQL Query Fundamentals 7.2 SOSL Search Fundamentals 7.3 DML Operations & Bulkification 7.4 Governor Limits & Transaction Boundaries
8 Apex Triggers & Order of Execution
8.1 Trigger Structure & Context Variables 8.2 Bulk Trigger Patterns & Best Practices 8.3 Save Order of Execution 8.4 Recursion, Cascading & Transaction Control
9 Asynchronous Apex
9.1 @future Methods 9.2 Queueable Apex 9.3 Batch Apex 9.4 Schedulable Apex & Job Orchestration
10 Security, Events & Schema Access
10.1 Sharing, FLS, CRUD & Injection Prevention 10.2 Platform Events Publish & Subscribe 10.3 Programmatic Schema Access (Describe)
11 Visualforce
11.1 Visualforce Pages & Markup 11.2 Standard, Custom Controllers & Extensions 11.3 Visualforce in Lightning Experience
12 Lightning Components
12.1 Lightning Component Framework Benefits 12.2 Aura Component Structure & Resources 12.3 Lightning Web Components Fundamentals 12.4 LWC Data Access: Wire, Imperative & Events
13 UI Composition & Apex Integration Patterns
13.1 Composing UI in Lightning Apps 13.2 UI Security, Performance & Accessibility Patterns 13.3 Apex Actions for Flow, LWC, and Agentforce
14 Apex Testing
14.1 Testing Framework & Coverage Requirements 14.2 Writing Unit Tests for Triggers & Classes 14.3 Test Data Strategies & Isolation 14.4 Executing Tests & Execute Anonymous Differences
15 Debugging & Developer Tools
15.1 Debug Logs & Monitoring 15.2 Developer Console, CLI, VS Code & Workbench 15.3 Sandboxes, Scratch Orgs & Environment Strategy
16 Deployment & Release
16.1 Change Sets & Metadata Deployment 16.2 Unlocked Packages, Metadata API & Salesforce DX 16.3 Release Readiness & Credential Maintenance
11.2 Standard, Custom Controllers & Extensions
Key Takeaways Explain This Topic AI
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:
- 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 (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
- 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
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
- 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 ( @testSetup or in-test inserts)
- Construct the controller: new MyController() or new 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 PageReference URLs, 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.