Sunday, October 8, 2017

Visualforce webservice callout synchronously & asynchronously

Overview

Recently we have received a business case to perform a webservice callout from our Custom Visualforce page.

There are following ways we can make callout from Visualforce page.
  • Asynchronously
  • Synchrounously

Approach: Asynchronous callout (@future method)


From visualforce page, first we will verify all standard and business validations and then perform callout and any error message during save will be displayed on the UI.

For example if Customer Name is blank if will show the error message in the UI and will not perform callout.

Visualforce

From commandbutton action, save method will get called.

<apex:page id="MyVisualforcePage" controller="MyController"  showHeader="false">
    <apex:pageMessages id="msgId"/>   
    <apex:form id="MyForm">
        <apex:inputText id="customerNm" value="{!CustomerName}"/><br/>
        <apex:commandButton value="Save" action="{!save}" reRender="msgId"/>
    </apex:form>
</apex:page>

Controller

In the save method, it will verify if customer name is blank and show the error in UI, otherwise call performRestCallout().


public class MyController
{
 public String CustomerName {get;set;}
 
 public PageReference save()
 { 
  if(String.isBlank(CustomerName))
  {
   ApexPages.Message msg = new ApexPages.Message(ApexPages.Severity.Error, 'Name cannot be null');
   ApexPages.addMessage(msg);
   return null;
  }
  else
  {
   //perpare JSON and then callout
   MyWebService.performRESTCallout(JSONString);
  }
  //navigate to view page
  PageReference pg =  (new ApexPages.StandardController (new CustomObject__c(Id=recordId))).view();
  pg.setRedirect(true);
  return pg;
 }
}

WebService Class

public class MyWebService
{
    @future(callout = true)
    public static void performRESTCallout(string JSONString)
    {
 //perform callout
    }
}

If any error messages from callout occurs that can be handled following ways:

(Taken from bob_buzzard's answer)

(1) Post a chatter message to the user
(2) Send an email to the user
(3) Create a custom object/setting, add the message to that and write a visualforce page for the sidebar that displays the message.


Approach: Synchronous callout (using commandbutton oncomplete method)


Salesforce doesn't allow to commit data and perform callout in a single transaction as it will throw error message "Uncommited task is pending".

The following approach works well.

From commandbutton's action, we will call save method, it will commit the data and then oncomplete method with the help of javascript and actionFunction we will perform second action and hence perform callout. So, there will be two different transactions and no more errors.

That looks good!!!

Now, what will happen if during save, UI validation and business validations to be performed and if any error occurs that needs to be shown on UI and will not perform oncomplete call.

Secondly, since it is synchronous call, so any exceptions or errors returned from webservice call must be displayed on the UI.

Here is the most tricky part (where I have spend hours to find the work around, finally Eric from StackExchange helped me retaining the error message in UI. My question & Eric's answer).

Visualforce

Here conditionally oncomplete needs to be called.


  • So hasError property has been defined on controller. 
  • Access hasError values in the outputPanel which is getting rendered from commandbutton reRender attribute
  • Using oncomplete javascript function, first verify if error exists (hasMessages == 'false') and then call submit action with the help of actionFunction.

Error will be displayed like this:



Thats nice!!!

<apex:page id="MyVisualforcePage" controller="myExampleController"  showHeader="false">
    <apex:pageMessages id="msgId"/>
    <script>
        function performCallOutMethod()
        {
            if(hasMessages == 'false') {
                performCallOut();
            }
        }
    </script>
    <apex:form id="myForm">
        <apex:actionFunction name="performCallOut" action="{!submit}" reRender="script-block,msgId"/>
        <apex:inputText id="customerNm" value="{!CustomerName}"/><br/>
        <apex:commandButton value="Save" action="{!save}"
                            oncomplete="return performCallOutMethod();" reRender="script-block,msgId"/>
    </apex:form>
 <!-- this is main trick -->
    <apex:outPutPanel layout="block" id="script-block">
        <script>
            var hasMessages = '{!hasError}';
        </script>
    </apex:outPutPanel>
</apex:page>

Controller

Save method will perform validations and then will commit the changes into database. If Customer name is blank then it will show error message.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class MyController
{
 public String CustomerName {get;set;}
 
 public Boolean hasError { 
  get { 
   return ApexPages.hasMessages(); 
  }  
 }
 
 public PageReference save()
 { 
         if(String.isBlank(CustomerName))
  {
          ApexPages.Message msg = new ApexPages.Message(ApexPages.Severity.Error, 'Name cannot be null');
   ApexPages.addMessage(msg);
   return null;
  }
                //commit all data here, not giving full code for simplicity.
                insert customObj;
  return null;
 }
 
 public PageReference submit()
 {
  try
  { 
   //prepare JSON string and perform callout
   MyWebService.performRESTCallout(JSONString);
   
   //navigate to view page
   PageReference pg =  (new ApexPages.StandardController (new CustomObject__c(Id=recordId))).view();
   pg.setRedirect(true);
   return pg;
  }
  catch (Exception ex)
  {
   System.debug('Error in submit ' + ex);
                        ApexPages.Message msg = new ApexPages.Message(ApexPages.Severity.Error, ex.getMessage());
                        ApexPages.addMessage(msg);
                        return null;
  }
 }
}

WebService Class

Just a simple method and if any error occurs it will be propagated to calling method and finally be displayed at UI.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class MyWebService
{
 public static void performRESTCallout(string JSONString)
        {
  try
  {
   MyWebService.performRESTCallout(JSONString);
  }
  catch(Exception ex)
  {
   throw new CustomException(ex.getMessage());
  }
 }
}

Conclusion



Which approach to be taken for implementation it will be based on business scenarios, data volumes, performance and that is a separate topic all together.

Here, I have tried to put my proof of concepts and approach to perform callouts from Visualforce and necessary error handling.

Hope it helps!

1 comment: