Invoke Apex from a Custom Button using a Visualforce Page

I have had a few occasions where I wanted to invoke Apex Code by clicking a button on a Page Layout.  Everywhere I looked, it always said I needed to have the button call an s-Control, which would then invoke Apex Code that’s written as a Web Service.  I am doing my best to avoid using s-Controls and figured there had to be a way using Visualforce instead of an s-Control.  The key was the action attribute on the <apex:page tag.

Suppose you wanted to add a button on your Opportunity page that automatically did something when you pushed the button.  You can’t execute Apex Code directly from a button.  You need something in the middle.  Let’s try to do it with a Visualforce page instead of an s-Control.

Controller

The controller has the main method in it to be executed. This method returns a PageReference (very important). Note that the method does not need to be a webservice.


public class VFController {

	// Constructor - this only really matters if the autoRun function doesn't work right
	private final Opportunity o;
	public VFController(ApexPages.StandardController stdController) {
		this.o = (Opportunity)stdController.getRecord();
	}
	
	// Code we will invoke on page load.
	public PageReference autoRun() {

		String theId = ApexPages.currentPage().getParameters().get('id');

		if (theId == null) {
			// Display the Visualforce page's content if no Id is passed over
			return null;
		}

		for (Opportunity o:[select id, name, etc from Opportunity where id =:theId]) {
			// Do all the dirty work we need the code to do
		}

		// Redirect the user back to the original page
		PageReference pageRef = new PageReference('/' + theId);
		pageRef.setRedirect(true);
		return pageRef;

	}

}

Visualforce

The Visualforce page is very simple. You really don’t need anything in there except the

The expected behavior is that the action parameter invokes code (the autoRun method) that runs before the page is initialized. That method returns a Page Reference that redirects the user back to the original page.


<apex:page standardController="Opportunity" 
 extensions="VFController" 
 action="{!autoRun}"
>
  <apex:sectionHeader title="Auto-Running Apex Code"/>
  <apex:outputPanel >
      You tried calling Apex Code from a button.  If you see this page, something went wrong.  You should have 
      been redirected back to the record you clicked the button from.
  </apex:outputPanel>
</apex:page>

Custom Button

All we need now is a custom Opportunity button. Because we used the Opportunity standard controller in the Visualforce page, we can simply have the button point to a the page we created.

Add that button to your Page Layout and all should work swimmingly.

33 Comments »

  1. George Scott Said,

    January 8, 2009 @ 6:40 pm

    Good post. FYI, you can simplify the controller extension a little bit as follows by using the standard controller instance in your autoRun() method as follows (it will probably not format well):

    public class VFController {

    private final ApexPages.StandardController theController;

    public VFController(ApexPages.StandardController stdController) {
    theController = stdController;
    }

    // Code we will invoke on page load.
    public PageReference autoRun() {
    for (Opportunity o:[select id, name from Opportunity where id =:theController.getId()]) {
    // Do all the dirty work we need the code to do
    }
    return theController.view().setRedirect(true);
    }

    }

  2. Dan Fowlie Said,

    January 8, 2009 @ 7:29 pm

    This is a good approach. For error handling you can add

    to the VisualForce page and then use something like this in the controller:

    if (ErrorCondition == true){
    ApexPages.Message myMsg = new ApexPages.Message(ApexPages.Severity.ERROR,’Add your message here’);
    ApexPages.addMessage(myMsg);
    return null;
    }

  3. Dan Fowlie Said,

    January 8, 2009 @ 7:33 pm

    My last comment had the VisualForce tag removed. apex:messages should be added to the Visualforce page.

  4. Tanner Wells Said,

    January 9, 2009 @ 5:10 am

    Great post Scott. I had only implemented calling apex from buttons using s-controls/webservices before and was hoping there was a good alternative via VisualForce.

  5. Benjamin Pirih Said,

    January 30, 2009 @ 9:16 pm

    Nice article Scott.

    Have you attempted invocation of the autorun method during the construction of the controller instance? This would reduce the need to explicitly define action=”{!autoRun}” and would reduce the underlying overhead associated with handling an action.

    Good Stuff, Thanks, Benjamin Pirih

  6. Scott Hemmeter Said,

    January 31, 2009 @ 2:12 pm

    @Ben

    That’d work too. I believe the action function runs before the constructor does, which means you don’t have the overhead. Not totally sure about that, though. If that’s the case, then it’s nice to have the constructor available for other things you might want other pages to do since you can re-use a constructor on multiple pages.

  7. Benjamin Pirih Said,

    January 31, 2009 @ 8:28 pm

    Hey Scott,

    FYI there is a discussion about this topic on the Community site which talks about the proper use of page actions vs. object construction..

    http://forums.sforce.com/sforce/board/message?board.id=Visualforce&message.id=6862&query.id=48537

    “In general you want any initialization code to happen in your constructor. That is the only place where you are guaranteed for that code to execute before anything else in your class (getters, setters, actions).

    Actions are generally for doing some kind of redirection, DML, or state change (say you want a checkbox flipped from true to false when a button is clicked). If you need load logic to happen that should probably happen in your constructor.”

    Very good stuff.. Thanks for the interesting information/discussion.. Ben

  8. Zach Said,

    February 25, 2009 @ 11:18 pm

    @Ben

    But if any of your autoRun code involves a record update, then calling that function from the constructor would result in a System.exception “DML not allowed here”, disqualifying it from being invoked from the constructor – am I correct?

    Example:
    // dirty work
    o.Name = ‘Bob’;
    update o;

    (I’d love to be wrong on this, by the way)

  9. DavidPSG Said,

    March 16, 2009 @ 10:46 am

    I used the material in this article recently and it works like a champ. But I’m having an awful time trying to write a test method for it. Any suggestions?

  10. Scott Hemmeter Said,

    March 16, 2009 @ 11:01 am

    @DavidPSG,

    If you are familiar with writing test methods in general, then it’s not that different than writing them for any other method.

    To invoke the method, you might need to do something like this…

    ApexPages.StandardController sc = new ApexPages.StandardController(theRecord);

    VFController controller = new VFController (theRecord);

    controller.autoRun();

    // other code to assert stuff

  11. david Said,

    March 16, 2009 @ 1:59 pm

    Thanks for the post, this was very helpful for me.

    BTW – is there an easy way to display a message during the autoRun process? I’ve created a VF page to override the standard lead conversion button. The apex takes a while to run so I want to use apex:OutputText to display “please wait … conversion in progress”, but of course the autorun happens first and then I redirect to the “conversion successful” page.

    Please wait … lead conversion in progress …

    Thanks
    David

  12. DavidPSG Said,

    March 17, 2009 @ 6:03 am

    Scott:

    Thanks very much for the quick reply.
    My controller is virtually identical to your example, except it acts on the Contract object rather than Opportunity, and of course the method itself is different. The test method (I thought) was very straightforward:

    static testmethod void TestCtrBn()
    {
    Contract cn = [select ID from Contract where CustomerSignedTitle = 'CtrBnController'];

    ApexPages.StandardController sc = new ApexPages.StandardController(cn.ID);
    CtrBnController controller = new CtrBnController(cn.ID);

    controller.UpdateTE();
    Integer t = [Select count() from Time_entry__c where Contract__c = :cn.ID];

    system.AssertEquals(3, t);
    }

    The problem is when I try ti save it I get these errors:

    Save error: Constructor not defined: [ApexPages.StandardController].(Id)

    Save error: Constructor not defined: [CtrBnController].(Id)

    I don’t want to take too much of your time, but was hoping a quick look might reveal my mistake.

    Thanks again.

  13. Scott Hemmeter Said,

    March 17, 2009 @ 9:44 am

    @DavidPSG, you need to modify the lines where you set the Standard Controller and setup the CtrBnController object.

    The first line takes the Contact object, not just the ID. The second line takes in the Standard Controller object you created in Line 1.

    ApexPages.StandardController sc = new ApexPages.StandardController(cn);

    CtrBnController controller = new CtrBnController(sc);

  14. DavidPSG Said,

    March 17, 2009 @ 11:15 am

    Scott:

    Great! That solved the instantiation problem.
    Thanks again for taking the time to help me out!

  15. BrianP Said,

    June 12, 2009 @ 6:58 am

    Using you code examples above i am haivng trouble getting the test method to work correctly. When i run the test method the code coverage is very low, because the test method runs selects one row, but then the autorun() method returns a null value.

    Any Ideas

    static testmethod void TestCtrBn()
    {
    Custom_Object__c cn = [select ID from Custom_Object__c where Custom_Object_Number__c = '1234'];

    ApexPages.StandardController sc = new ApexPages.StandardController(cn);
    VFConvertTrade controller = new VFConvertTrade(sc);

    controller.autoRun();

  16. Scott Hemmeter Said,

    June 12, 2009 @ 7:32 am

    @BrianP,

    I’d have to see your code to know for sure. One thing I do know is that you should be creating your custom object data in your test class, not selecting existing data. If your autoRun code handles many different data scenarios, create custom object data in your test class to accommodate each and call autorun for each record.

    If you want to, you can log a case and show me your code and we can go from there.

  17. Craig Harris Said,

    July 13, 2009 @ 8:53 am

    I appear to have solved this a different way.

    1) Create a controller extension (in my case Account)

    public with sharing class AccountControllerExtension {

    private final Account acct;

    public AccountControllerExtension(ApexPages.StandardController stdController) {
    this.acct = (Account)stdController.getRecord();
    }

    public PageReference doAction() {
    // Call other Apex classes to perform action

    // Return a page ref
    PageReference customPage = new PageReference(‘/apex/NewPage’);
    customPage.getParameters().put(‘id’, acct.Id);
    return customPage;
    }
    }

    2) Create a page that overrides the standard view

    3) Override the Account view Setup -> Customize -> Account -> Buttons and Links

    4) Add a custom button (Execute Javascript) with the following body

    doAction();

    How it works:

    The default Action view is replace with my custom page that is identical to the original with the exception of the hidden form with the hidden actionFunction and the inclusion of the controller extension
    When the custom button is pressed it invokes the actionFunction which in turn invokes the action on the controller extension.
    The action on the controller returns a PageReference (with redirect attribute set) to direct the user to a new screen, or back to the current screen using ApexPages.currentPage()

  18. Invoking Apex from a Button (JS –> Apex Web Service) | Perspectives on Salesforce.com Said,

    July 13, 2009 @ 9:54 am

    [...] January I posted about how to invoke Apex from a Custom Button using a Visualforce Page.  It has been a popular post and is a topic which is of interest to many developers.  I wanted to [...]

  19. » Sites & Visualforce: Some early lessons (The Enforcer.net, a force.com blog) Said,

    July 28, 2009 @ 5:10 am

    [...] trigger field updates via a link , look at Scott Hemmeter’s article on how to Invoke Apex from a Custom Button using a Visualforce Page [...]

  20. Osama's Weblog » Invoke APEX code from custom button Said,

    January 11, 2010 @ 12:32 pm

    [...] is a really good example of invoking apex code through apex code here. But I have found another way to invoke the apex method without using visualforce [...]

  21. Andy Forbes Said,

    February 18, 2010 @ 7:41 am

    I’m having the same problem as BrianP. I can’t seem to find a way to get this statement in the controller:

    String theId = ApexPages.currentPage().getParameters().get(‘id’);

    To return anything but null.

    I see that the last comment on this blog was half a year ago :-) , but if you’re still checking it any insight would be greatly appreciated.

    Test code:

    @isTest
    private class CancelEventTest {

    static testMethod void CETest() {

    Profile currentProfile = [select id from profile where name='System Administrator'];
    Userrole currentRole = [select id from UserRole where Name = 'Administration'];
    User currentUser = new User(alias = ‘admin’, email=’admin@testorg.com’,
    emailencodingkey=’UTF-8′, lastname=’Testing’, languagelocalekey=’en_US’,
    localesidkey=’en_US’, profileid = currentProfile.Id,
    timezonesidkey=’America/Los_Angeles’, username=’admin@testorg.com’,
    userroleid = currentRole.Id);

    Event currentEvent = new Event(ownerid=currentUser.Id, subject=’Test’,
    startdatetime=System.Now(), enddatetime=System.Now());

    ApexPages.StandardController sc = new ApexPages.StandardController(currentEvent);
    CancelEventController CETestController = new CancelEventController(sc);

    PageReference pageRef = new PageReference(‘/’ + currentEvent.ID);
    Test.setCurrentPage(pageRef);

    CETestController.autorun();

    }
    }

  22. Scott Hemmeter Said,

    February 18, 2010 @ 10:39 am

    @Andy, the blog is active. It’s been 6 months since a comment on this post, but the blog itself has received comments as recently as today.

    The issue is in your pageRef line. First of all, put the pageReference lines above your Event currentEvent lines. Second, you need to create a reference to your Visualforce page. Should be something along the lines of:


    PageReference pageRef = Page.yourVFPageName;
    pageRef.getParameters().put('id', String.valueOf(currentEvent.ID));
    Test.setCurrentPage(pageRef);

  23. Paul Said,

    August 6, 2010 @ 7:17 am

    Never mind my last. Got it working. I can’t tell you how psyched I am. This was my last major crux to cross and now I can really run with my project. Thanks a million for this invaluable post!

  24. Paul Said,

    August 12, 2010 @ 3:40 pm

    Hi Scott,

    I’m building unit tests and having the same trouble Andy was experiencing:

    String theId = ApexPages.currentPage().getParameters().get(‘id’);
    Can’t get this to return anything but null.

    All I have done is change the sObj to a Task vs. your orig. Opportunity example. I saw this problem earlier when my app wasn’t correctly referencing the VF page.

    Here’s the unit test code. Do you see anything obviously wrong? I’m also only getting 50% coverage, but that’s probably another story.

    @isTest // code won’t count against overall Apex code size limit in org
    private class VFControllerTest {

    static testmethod void TestVFController()
    {
    // I should create a contact and then creat a task
    Account t_account = new Account(name=’Some Company’);

    Contact t_contact = new Contact();
    // Could set all these fields in the constructor
    t_contact.AccountID = t_account.Id;
    t_contact.FirstName = ‘foo’;
    t_contact.LastName = ‘bar’;
    t_contact.Email = ‘foo.bar@gmail.com’;

    Task t_task = new Task();
    t_task.whoID = t_contact.id;
    t_task.subject = ‘VFTest’;

    PageReference pageRef = Page.VFTaskMachine;
    pageRef.getParameters().put(‘id’, String.valueOf(t_task.ID));

    Test.setCurrentPage(pageRef);
    String theId = ApexPages.currentPage().getParameters().get(‘id’);
    System.debug(‘UNIT TEST 1 theID = ‘ + theID);

    ApexPages.StandardController sc = new ApexPages.StandardController(t_task);
    VFController VFTestController = new VFController(sc);

    VFTestController.autorun();
    System.debug(‘UNIT TEST 2 theID = ‘ + theID);

    }

    Thanks for any help you can offer.

  25. GON Said,

    March 22, 2011 @ 1:56 am

    Hi Scott,
    I´m new to salesforce, and i´m trying to update a field when a user clicks a button in a layout page.
    I´m using the code above, but i only get the same page without any update in the field.
    I would really appreciate your feedback.
    Here is my code:

    public class VFController
    {
    // Constructor – this only really matters if the autoRun function doesn’t work right
    private final RFB_Capacidad__c o;
    public VFController(ApexPages.StandardController stdController)
    {
    this.o = (RFB_Capacidad__c)stdController.getRecord();
    }
    // Code we will invoke on page load.
    public PageReference autoRun()
    {
    String theId = ApexPages.currentPage().getParameters().get(‘id’);
    if (theId == null)
    {
    // Display the Visualforce page’s content if no Id is passed over
    return null;
    }

    for (RFB_Capacidad__c o:[select Estado_de_Facturacion__c from RFB_Capacidad__c where id =:theId])
    {
    o.Estado_de_Facturacion__c=’Pendiente de Revisar’;
    }
    // Redirect the user back to the original page
    PageReference pageRef = new PageReference(‘/’ + theId);
    pageRef.setRedirect(true);
    return pageRef;
    }
    }

    The goal is to set the value of the picklist “Estado_de_Facturacion__c” to “Pendiente de Revisar”
    hope you can help me!
    Thanks in advance!

  26. Scott Hemmeter Said,

    March 22, 2011 @ 8:16 am

    @GON,

    Not sure exactly why you are doing what you are doing (I’d just do a mass update of the actual data and not need this code), but the reason nothing is sticking is that you are not updating the record and are then redirecting the user away from the page they are on and refreshing the page. You are doing the equivalent of going to a record, clicking Edit and then clicking the Back button on your browser without saving the record.

  27. Gary Pinkham Said,

    May 3, 2011 @ 7:03 am

    I tried to use Dan Fowlie’s way of adding a message (in my case I want to add it regardless of error). but I’m doing this with a standard page (the Case Support page to be exact). But a message doesn’t show.. So I’m not sure if I’m doing something wrong or of the page doesn’t have the apex:messages tag.. I can’t see the visiualforce code for a standard page (at least I don’t think I can).

    So any suggestions on figuring out if its the message code or the page is missing the required tags?

    Just a note.. the code of the controller calls a web service that spawns a job so it returns immediately.. I want to inform the user that the request was submitted..

    this is the code I inserted in my custom controller that is called from the custom button..
    // Redirect the user back to the original page
    ApexPages.Message myMsg = new ApexPages.Message(ApexPages.Severity.INFO, ‘Request Submitted’);
    ApexPages.addMessage(myMsg);
    PageReference pageRef = new PageReference(‘/’ + theId);
    pageRef.setRedirect(true);
    return pageRef;

    Thanks in advance!!

  28. Rakesh Said,

    January 6, 2012 @ 2:07 pm

    Hi I tried this approach but it is throwing the error
    “Insufficient Privileges
    You do not have the level of access necessary to perform the operation you requested. Please contact the owner of the record or your administrator if access is necessary. ”

    when clicking on the button please help

  29. Sam Said,

    March 9, 2012 @ 8:13 pm

    I’m sure this is a dumb question, but what happens if someone clicks my custom button while they still have unsaved changes on the page?

    For example, my staff have to put in several pieces of data on an Opportunity and then I want them to click my custom button that will use those pieces of new data to create a new related record. Do I have to train them to put in the new pieces of info and hit “Save”, wait for page to refresh, and then hit my custom button? Or, can they put in the new pieces and hit my custom button which will save any unsaved changes before doing its work?

  30. Scott Hemmeter Said,

    March 10, 2012 @ 2:58 pm

    Sam, custom buttons only appear on the View page. They cannot click the button when editing a record.

  31. Sam Said,

    March 11, 2012 @ 10:10 am

    thanks, Scott! now that you mention it – duh!! i completely forgot that custom buttons disappear when there are unsaved changes.

  32. Updating Standard Salesforce object with custom button | Joshua Pyle Said,

    March 27, 2013 @ 6:53 pm

    [...] I am trying to update the status field of the standard salesforce contract object. Since we are using a custom layout, it would be preferable to simply have a button on top to accomplish this. I have tried using the method described at: http://sfdc.arrowpointe.com/2009/01/08/invoke-apex-from-a-custom-button-using-a-visualforce-page/ [...]

  33. How to invoke custom APEX code from Standard Salesforce Button | Code Jag Said,

    August 23, 2013 @ 11:34 am

    [...] Here is the link to check out Scott’s Blog:  http://sfdc.arrowpointe.com/2009/01/08/invoke-apex-from-a-custom-button-using-a-visualforce-page/ [...]

RSS feed for comments on this post · TrackBack URI

Leave a Comment

All comments are moderated. Other visitors will not see your comment until it has been approved.