Make Long-Running Callouts from Visualforce pages using the Continuation pattern - El Toro - Find articles about Visualforce, Apex, Force.com and Salesforce in general

Print Preview

Make Long-Running Callouts from Visualforce pages using the Continuation pattern

As you may be familiar, Salesforce has a Governor Limit that meassures the "Number of synchronous concurrent requests for long-running requests that last longer than 5 seconds". As of Winter 16, the value is 10.

One way to get around this is to perform @future methods, but if you are working on a Visualforce page you'll never hear back from this @future method and your users won't be notified of the result. In that case, how can you build a Visualforce page that shows a "please Wait" message to the user and once the results are complete, the page refreshes and shows the results from the web service?

Good question, I am glad you asked. Here comes the Continuation object to the rescue...

As mentioned in the documentation, the continuation is "An asynchronous callout that is made from a Visualforce page for which the response is returned through a callback method."

The basic idea, is to have an action method in your controller that returns a Continuation object, rather than a Page Layout, and makes an asynchronous request to a webservice (either SOAP or REST). When the server responds, a callback method is invoked and the screen can be updated at this time.

Couple very important things to note:

  • This Continuation pattern does not allow you to make DML operations before the webservice is invoked.
  • There coud be DML operations while¬†the Asynchronous webservice callout is completed.
  • The server has to return within 2 minutes, or there will be a timeout

With the Continuation method, I have created a page that looks like this:

While we are waiting for the web service server to return, the user will see a modal screen based on this other article. The screen looks like this:

The code for this sample, includes:

  • A visualforce page
  • The controller
  • The helper class that makes the REST callout and handles the results.

The Visualforce page for the Continuation

<apex:page controller="Pause">
    <style>
        /* This is for the full screen DIV */
        .popupBackground {
        /* Background color */
        background-color:black;
        opacity: 0.20;
        filter: alpha(opacity = 20);
        
        /* Dimensions */
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        z-index: 998;
        position: absolute;
        
        /* Mouse */
        cursor:wait;
        }
        
        /* This is for the message DIV */
        .PopupPanel {
        /* Background color */
        border: solid 2px blue;
        background-color: white;
        
        /* Dimensions */
        left: 50%;
        width: 200px;
        margin-left: -100px;
        top: 50%;
        height: 50px;
        margin-top: -25px;
        z-index: 999;
        position: fixed;
        
        /* Mouse */
        cursor:pointer;
        }
    </style>
    <apex:form >
        <apex:pageBlock id="Counter">
            <apex:pageBlockButtons location="top">
                <apex:commandButton value="Plus 1" action="{!Plus1}"
                                    rerender="Counter" status="statusCounter"/>
                <apex:commandButton value="Refresh data" action="{!RefreshData}"
                                    rerender="Counter" status="statusCounter"/>
                <apex:commandButton action="{!startRequest}" value="Start Request"
                                    rerender="Continued" status="statusContinuation"/>
                <apex:actionStatus id="statusCounter" startText="Please Wailt" stopText="" />
            </apex:pageBlockButtons>
            <apex:pageBlockSection >
                <apex:pageBlockSectionItem >
                    Counter
                    <apex:outputText value="{!Counter}" />
                </apex:pageBlockSectionItem>
                <apex:pageBlockSectionItem >
                    # Employees
                    <apex:outputText value="{!a.NumberOfEmployees}" />
                </apex:pageBlockSectionItem>
                <apex:pageBlockSectionItem >
                    Seconds
                    <apex:inputText value="{!Seconds}" />
                </apex:pageBlockSectionItem>
            </apex:pageBlockSection>
        </apex:pageBlock>
        <hr/>
        <apex:outputPanel id="Continued">
            <apex:pageBlock rendered="{!Continued}" >
                <apex:pageBlockSection >
                    <apex:pageBlockSectionItem >
                        Start
                        <apex:outputText value="{!results.startDTTM}" />
                    </apex:pageBlockSectionItem>
                    <apex:pageBlockSectionItem >
                        End
                        <apex:outputText value="{!results.endDTTM}" />
                    </apex:pageBlockSectionItem>
                </apex:pageBlockSection>
            </apex:pageBlock>        
        </apex:outputPanel>
    </apex:form>
    <apex:actionStatus id="statusContinuation" stopText="">
        <apex:facet name="start">
            <div>
                <div class="popupBackground" />
                <div class="PopupPanel">
                    <table border="0" width="100%" height="100%">
                        <tr>
                            <td align="center"><b>Please Wait</b></td>
                        </tr>
                        <tr>
                            <td align="center"><img src="{!$Resource.ProgressBar}"/></td>
                        </tr>
                    </table>
                </div>
            </div>
        </apex:facet>
    </apex:actionStatus>
</apex:page>

Visualforce Controller

public class Pause {
    // TESTS
    public Account a { get; set; }
    public Integer counter { get; set; }
    public Integer seconds { get; set; }
    public Boolean Continued { get; set; }
    public demoContinuationREST.Results results { get; set; }

    public Pause() {
        seconds = 3;
        Continued = false;
        RefreshData();
        counter = a.numberOfEmployees;
    }

    public PageReference Plus1() {
        counter++;
        a.NumberOfEmployees = counter;
        update a;
        RefreshData();
        return null;
    }
    public PageReference RefreshData() {
        a = [SELECT ID, numberOfEmployees FROM Account
             WHERE ID = :ApexPages.currentPage().getParameters().get('id')];
        return null;
    }

    // CONTINUATION
    // Unique label corresponding to the continuation
    private String requestLabel;

    // Method to start the Continuation Webservice
    public Object startRequest() {
        // You can't perform DMLs before calling the webservice, not
        // even on a Continuation pattern
        /*
        Account a = [SELECT ID FROM Account LIMIT 1];
        update a;
        */
        
        // Create continuation with a timeout
        Continuation con = new Continuation(120);
        // Set callback method
        con.continuationMethod='processResponse';
        
        // Call webservice using Continuation
        requestLabel = demoContinuationREST.sleep(con, seconds);
        
        Continued = false;
        
        // Return the continuation
        return con; 
    }
    
    // Callback method 
    public Object processResponse() {
        HttpResponse response = Continuation.getResponse(this.requestLabel);
        System.debug('response: ' + response);
        String result = demoContinuationREST.getBody(response.getBodyDocument());
        System.debug('result: ' + result);
		results = new demoContinuationREST.Results(result);
        System.debug('results: ' + results);
        Continued = true;
        
        // Return null to re-render the original Visualforce page
        return null;
    }    
}

Helper class

public class demoContinuationREST {
    public static String sleep(Integer seconds) {
        return sleep(null, seconds);
    }
    public static String sleep(Continuation con, Integer seconds) {
        
        HttpRequest req;
        
        String xml = '';
        xml += '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:aper="APerezConsulting">';
        xml += '<soapenv:Header/>';
        xml += '<soapenv:Body>';
        xml += '<aper:PauseSeconds>';
        xml += '<aper:seconds>' + seconds + '</aper:seconds>';
        xml += '</aper:PauseSeconds>';
        xml += '</soapenv:Body>';
        xml += '</soapenv:Envelope>';
        
        req = new HttpRequest();
        req.setEndpoint('http://www.XXXXXXXX.com/Pause.asmx');
        req.setMethod('POST');
        req.setHeader('SOAPAction', '"APerezConsulting/PauseSeconds"');
        req.setHeader('Content-Type', 'text/xml;charset=UTF-8');
        req.setBody(xml);
        
        if (con == null) {
            HTTP h = new Http();
            HttpResponse resp = h.send(req);
            if (resp.getStatusCode() == 200) {
                String msg = getBody(resp.getBodyDocument());
                System.debug(msg);
                return msg;
            }
            
            String msg = '';
            msg += 'HTTP Response #' + resp.getStatusCode();
            msg += ': ';
            msg += resp.getBody();
            throw new demoException(msg);
        } else {
            String requestLabel = con.addHttpRequest(req);
            return requestLabel;
        }
    }
    
    public static String getBody(String xml) {
        Dom.Document doc = new Dom.Document();
        doc.load(xml);
        return getBody(xml);
    } 
    public static String getBody(Dom.Document doc) {
        Dom.XMLNode node = doc.getRootElement();
        node = node.getChildElement('Body', 'http://schemas.xmlsoap.org/soap/envelope/');
        node = node.getChildElement('PauseSecondsResponse', 'APerezConsulting');
        node = node.getChildElement('PauseSecondsResult', 'APerezConsulting');
        return node.getText();    
    }
    
    public class Results {
        public String startDTTM { get; set; }
        public String endDTTM { get; set; }
        
        public Results(String strFromWS) {
            strFromWS = strFromWS.replace('][', '|');
            strFromWS = strFromWS.remove('[');
            strFromWS = strFromWS.remove(']'); 
            List<String> parts = strFromWS.split('\\|');
            startDTTM = removeLabel(parts[0]);
            endDTTM = removeLabel(parts[1]);
        }
        
        private String removeLabel(String original) {
            return original.remove(original.mid(0, original.indexOf(':')+1)).Trim();
        }
    }
}

comments powered by Disqus

© El Toro . IT @ 2013
Andrés Pérez