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. 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.
<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


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);
}
}
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;
}
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.
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.
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
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.
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
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)
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?
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…
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
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:
The problem is when I try ti save it I get these errors:
I don’t want to take too much of your time, but was hoping a quick look might reveal my mistake.
Thanks again.
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.
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!
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();
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.