BlogSalesforce

On-View triggers in Salesforce (trigger for Opportunity Contact Roles)

By August 30, 2012 11 Comments

Native Opportunity Limitations

One current limitation with Salesforce.com native Opportunities model is the inability to have a trigger on the Opportunity Contact Role object. Say you have built a custom Opportunity rollup or scoring mechanism of some variety and it relies upon what/who the user selects in the Opportunity Contact Role area: how will you detect a live change in related Opportunity Contact Roles? You might program a regular Apex schedule… however, it wouldn’t really be “live” and it would use up one of those cherished Apex Schedule slots.

Here’s a trick to detect such a change using Visualforce and native Opportunity layouts.
It’s really about having your code “know” (and possibly execute) every time a user visits a native Salesforce layout (regardless of the object.) Hence, we could call it an “On-View” trigger.

Start with an Apex Controller

Write a very simple Apex controller that has a constructor and one method with a future annotation. The future annotation will ensure there’s no lack time when the page is loaded due to the “hidden” Apex processing you’ll be doing:

public with sharing class OppHelper {

public Opportunity opp; public OppHelper( ApexPages.StandardController stdController ) { opp = ( Opportunity )stdController.getRecord(); } public void rollupOppContacts(){ OppHelper.rollupOppContactsFuture( opp.Id ); } @future public static void rollupOppContactsFuture( Id oppId ) { Contact[] contactList = [ SELECT Id, Some_field__c FROM Contact WHERE Id IN ( SELECT ContactId FROM OpportunityContactRole WHERE OpportunityId = :oppId ) ]; Opportunity opp = [ SELECT Id, Some_rollup_score__c FROM Opportunity WHERE Id = :oppId ]; opp.Some_rollup_score__c = 0; for( Contact contact : contactList ) { opp.Some_rollup_score__c = contact.Some_field__c; } update opp; } }

Move Onto a Visualforce Page

Then write a very simple Visualforce page that uses the “action” attribute in the apex:page markup:

<apex:page standardController="Opportunity" extensions="OppHelper" action="{!rollupOppContacts}" />

Add the Visualforce page to any/all Opportunity layouts with width and height settings set to zero.

Every time the user navigates to an Opportunity layout the code will execute. Given that the only way a user can make a change to Opportunity Contact Roles is through some sort of “native” Opportunity layout, you’re ensured that this will trigger for every such change. (The exception being if you’re org already has some custom Apex/VF that allows/makes changes to Opportunity Contact Roles… in which case you could do the appropriate rollup/scoring etc.. within that.)

If a user makes a change to Opportunity Contact Roles, after clicking save, Salesforce brings the user back to the Opportunity layout by default. Your code will execute again with the freshest Opportunity Contact Role data.

Lastly, note that using the future annotation means it won’t be real time. However, the page will load without delay.

Questions? Talk with a Salesforce Developer

Our team of experienced developers and architects can help you customize your Salesforce solution with many kinds of triggers and other functionality. Contact us with your questions and ideas today.

Tom Burre

Tom Burre

Tom specializes in Saleforce.com. When not helping clients build custom Force.com applications or enhance Salesforce CRM functionality, Tom can be found toying with strings of the tennis, musical and of course "type" variety.

11 Comments

  • Avatar RS says:

    Thanks for this code. I was looking something similar for NPSP implementation.

  • Avatar Jin Daikoku says:

    Very helpful. Thank you!

  • Avatar Tim Milazzo says:

    This has shown to be very helpful – one of the few solutions to workaround the fact that Contact Roles do not support triggers.

    One question – How long does it take to calculate when you include “@future”? Is it simply an asynchronous load with the page, i.e. it should only take a couple of seconds?

  • Avatar JD says:

    If the contact record gets deleted and you need to do something with the contact role after deleted, this solution will not work.

  • Avatar Artie Brosius says:

    @JD: that's true, however to cover that use case, I would suggest creating a delete trigger on the Contact object.  During that trigger, you can gather the Opportunity Contact Roles for that Contact and perform whatever operations you wish.

  • Avatar MCamp says:

    Tom,
    This is great, I moved the @future out into another class so I can call it from other code. So what would the test class/method look like for the controller extension portion?

  • Avatar Rich says:

    Forgive my ignorance on this issue as I’m no developer, by Apex Controller do you mean adding this code as a new Apex Trigger or Class?

  • Taylor Kingsbury Taylor Kingsbury says:

    Hi Rich,

    The Apex Controller Tom mentions in this example, OppHelper, is an Apex Class, not a trigger. It is actually a controller extension, which is used when you want to add a bit of extra functionality to what can be done with Opportunities.

  • Avatar Jason Bury says:

    Hi MCamp,

    You can test the controller constructor rather easily, as if it were any other Apex class.  Something like the below test method will help you verify its behavior:

    	@isTest
        private static void testConstructor() {
    		Opportunity opp = new Opportunity(
                Id = Opportunity.sObjectType.getDescribe().getKeyPrefix() + '000000000001'
            );
            
            ApexPages.StandardController sc = new ApexPages.StandardController(opp);
    
    		OppHelper oh = new OppHelper(sc);
            
            System.assertEquals(opp, oh.opp); 
        }
    

     

    As for testing the behavior of the rollupOppContacts method, it is a little tricker.  Unfortunately, static calls like this are notoriously difficult to test.  Ideally, you'd test this method by calling it in a test and then verifying that the future job has been scheduled and that it received the correct id parameter.  However, you can't verify that a future method has been scheduled in a test (as far as I know) because the AsyncApexJob that is normally created for future methods is not created in a test context.

    One thing you COULD do, though, is to call the rollupOppContacts method in a test, bracketed by Test.startTest() and Test.stopTest() methods, which will cause the future method to run before code following this block is executed, and then you can verify that the side effects expected from the future method itself are present.  Your test method for this would look something like this:

        @isTest
        private static void testRollupOppContacts() {
            Opportunity opp = new Opportunity(
                Name = 'TestOpportunity',
                CloseDate = Date.today(),
                StageName = 'Negotiation/Review'
            );
    
            insert opp;
            
            OppHelper oh = new OppHelper(new ApexPages.StandardController(opp));
            
            oh.opp = opp;
    
            Test.startTest();
            oh.rollupOppContacts();
            Test.stopTest();
    
            // at this point the rollupOppContactsFuture method would have completed,
            // so run asserts accordingly to verify behavior
        }
    

    I realize that may not be a satisfying answer.  If 100% test coverage and true unit testing is important to you, I would encourage you to consider how to refactor this logic such that you could use the ApexMocks framework to verify each method as a unit, without the dependencies in the above code.

    Best,

    Jason

  • Avatar NeilC says:

    Can someone give me some idea of how to write a test for the OppHelper class?

  • Avatar Updesh Singh says:

    The code was very helpful, thanks

Leave a Reply

Need to adjust your business processes quickly? We're helping clients use technology to keep their teams productive and running smoothly in these times of uncertainty. Our team can guide yours if you need help in these areas.

Talk to a Consultant