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.