Saturday, 8 September 2012

Making rejection comments mandatory in approval processes

When I was recently asked to set up an approval process on a custom object in a application, I noticed that it was possible to reject an approval request without having to populate the comment field.

This didn't seem to make much sense to me, if an approval is rejected, it is likely that the requesting user will want to know why this is the case. I was surprised to find that there was no setup option to configure the process this way. 

I found that it is possible to make comments mandatory through adding a custom field, some extra steps to your approval process, and an apex trigger for the object. Here is a step by step guide if you want to do the same:

1) Create a new picklist custom field for your object. Set the label as "Approval Status". Add three picklist values ("Pending", "Approved" & "Rejected"). Make this field visible and editable by System Administrators only.




2) Navigate to the field updates menu (Setup --> App Setup --> Create --> Workflows & Approvals --> Field Updates) and create a new field update worfkflow action. Set the name of the new action to "Approval Status Pending" and unique name to "Approval_Status_Pending" and object to the object you added the custom field to. Then select the Approval Status field from the resulting drop down. In the new field value options, select "A specific value" then pick "Pending" status.



3) Repeat step 2 for the "Approved" and "Rejected" picklist values, creating field updates called "Approval Status Approved" and "Approval Status Rejected" respectively.



4) Navigate to your approval process. In "Initial Submission Actions" add the "Approval Status Pending" field update action. Add "Approval Status Approval" update to "Final Approval Actions" and "Approval Status Rejection" to "Final Rejection Actions".



5) Create a new "RequireRejectionComment" before update Apex trigger on your object with the following body (substituting "Invoice_Statement__c" for your object API name).

trigger RequireRejectionComment on Invoice_Statement__c (before update) 
{

  Map<Id, Invoice_Statement__c> rejectedStatements 
             = new Map<Id, Invoice_Statement__c>{};

  for(Invoice_Statement__c inv: trigger.new)
  {
    /* 
      Get the old object record, and check if the approval status 
      field has been updated to rejected. If so, put it in a map 
      so we only have to use 1 SOQL query to do all checks.
    */
    Invoice_Statement__c oldInv = System.Trigger.oldMap.get(inv.Id);

    if (oldInv.Approval_Status__c != 'Rejected' 
     && inv.Approval_Status__c == 'Rejected')
    { 
      rejectedStatements.put(inv.Id, inv);  
    }
  }
   
  if (!rejectedStatements.isEmpty())  
  {
    // UPDATE 2/1/2014: Get the most recent approval process instance for the object.
    // If there are some approvals to be reviewed for approval, then
    // get the most recent process instance for each object.
    List<Id> processInstanceIds = new List<Id>{};
    
    for (Invoice_Statement__c invs : [SELECT (SELECT ID
                                              FROM ProcessInstances
                                              ORDER BY CreatedDate DESC
                                              LIMIT 1)
                                      FROM Invoice_Statement__c
                                      WHERE ID IN :rejectedStatements.keySet()])
    {
        processInstanceIds.add(invs.ProcessInstances[0].Id);
    }
      
    // Now that we have the most recent process instances, we can check
    // the most recent process steps for comments.  
    for (ProcessInstance pi : [SELECT TargetObjectId,
                                   (SELECT Id, StepStatus, Comments 
                                    FROM Steps
                                    ORDER BY CreatedDate DESC
                                    LIMIT 1 )
                               FROM ProcessInstance
                               WHERE Id IN :processInstanceIds
                               ORDER BY CreatedDate DESC])   
    {                   
      if ((pi.Steps[0].Comments == null || 
           pi.Steps[0].Comments.trim().length() == 0))
      {
        rejectedStatements.get(pi.TargetObjectId).addError(
          'Operation Cancelled: Please provide a rejection reason!');
      }
    }  
  }
}

The trigger captures any update where the approval status is changed to "rejected" from another value. The field should have this value if it gets rejected, thanks to the way we set up the approval process. 

If the field has been updated in this way, a SOQL query is used to analyse the last created rejection approval history object related to the trigger object. If the comment is empty, the update gets stopped, and an error message is added to the screen.

Here are some screen shots of rejecting a request in chatter, and the resulting error message:



So there you have it, now all your rejections must have a comment, otherwise the operation is reversed! Because the rejection logic is handled in a trigger, this approach works for rejections through the standard pages, in chatter, and in Apex code.

Update 30/12/2013 : here is a sample test method for the trigger, hope it helps, remember to adapt it for your own implementation
/*
    A sample test class for the Require Rejection Comment trigger
*/
@isTest
public class RequireRejectionCommentTest
{
    /*
        For this first test, create an object for approval, then
        simulate rejeting the approval with an added comment for explanation.
        
        The rejection should be processed normally without being interrupted.
    */
    private static testmethod void testRejectionWithComment()
    {
        // Generate sample work item using utility method.
        Id testWorkItemId = generateAndSubmitObject();
        
        // Reject the submitted request, providing a comment.
        Approval.ProcessWorkitemRequest testRej = new Approval.ProcessWorkitemRequest();
        testRej.setComments('Rejecting request with a comment.');
        testRej.setAction  ('Reject');
        testRej.setWorkitemId(testWorkItemId);
    
        Test.startTest();        
            // Process the rejection
            Approval.ProcessResult testRejResult =  Approval.process(testRej);
        Test.stopTest();
        
        // Verify the rejection results
        System.assert(testRejResult.isSuccess(), 'Rejections that include comments should be permitted');
        System.assertEquals('Rejected', testRejResult.getInstanceStatus(), 
          'Rejections that include comments should be successful and instance status should be Rejected');
    }
    
    /*
        For this test, create an object for approval, then reject the request, mark the approval status as pending, then
        without a comment explaining why. The rejection should be halted, and
        and an apex page message should be provided to the user.
    */
    private static testmethod void testRejectionWithoutComment()
    {
        // Generate sample work item using utility method.
        Id testWorkItemId = generateAndSubmitObject();
        
        // Reject the submitted request, without providing a comment.
        Approval.ProcessWorkitemRequest testRej = new Approval.ProcessWorkitemRequest();
        testRej.setComments('');
        testRej.setAction  ('Reject');      
        testRej.setWorkitemId(testWorkItemId);
    
        Test.startTest();        
            // Attempt to process the rejection
            try
            {
                Approval.ProcessResult testRejResult =  Approval.process(testRej);
                system.assert(false, 'A rejection with no comment should cause an exception');
            }
            catch(DMLException e)
            {
                system.assertEquals('Operation Cancelled: Please provide a rejection reason!', 
                                    e.getDmlMessage(0), 
                  'error message should be Operation Cancelled: Please provide a rejection reason!'); 
            }
        Test.stopTest();
    }
    
    /*
        When an approval is approved instead of rejected, a comment is not required, 
        mark the approval status as pending, then ensure that this functionality still holds together.
    */
    private static testmethod void testApprovalWithoutComment()
    {
        // Generate sample work item using utility method.
        Id testWorkItemId = generateAndSubmitObject();
        
        // approve the submitted request, without providing a comment.
        Approval.ProcessWorkitemRequest testApp = new Approval.ProcessWorkitemRequest();
        testApp.setComments ('');
        testApp.setAction   ('Approve');
        testApp.setWorkitemId(testWorkItemId);
    
        Test.startTest();        
            // Process the approval
            Approval.ProcessResult testAppResult =  Approval.process(testApp);
        Test.stopTest();
        
        // Verify the approval results
        System.assert(testAppResult.isSuccess(), 
                     'Approvals that do not include comments should still be permitted');
        System.assertEquals('Approved', testAppResult.getInstanceStatus(), 
           'All approvals should be successful and result in an instance status of Approved');
    }
    
    /*
        Put many objects through the approval process, some rejected, some approved,
        some with comments, some without. Only rejctions without comments should be
        prevented from being saved.
    */
    private static testmethod void testBatchRejctions()
    {
        List<Invoice_Statement__c> testBatchIS = new List<Invoice_Statement__c>{};
        for (Integer i = 0; i < 200; i++)
        {
            testBatchIS.add(new Invoice_Statement__c());
        }   
           
        insert testBatchIS;
        
        List<Approval.ProcessSubmitRequest> testReqs = 
                         new List<Approval.ProcessSubmitRequest>{}; 
        for(Invoice_Statement__c testinv : testBatchIS)
        {
            Approval.ProcessSubmitRequest testReq = new Approval.ProcessSubmitRequest();
            testReq.setObjectId(testinv.Id);
            testReqs.add(testReq);
        }
        
        List<Approval.ProcessResult> reqResults = Approval.process(testReqs);
        
        for (Approval.ProcessResult reqResult : reqResults)
        {
            System.assert(reqResult.isSuccess(), 
                          'Unable to submit new batch invoice statement record for approval');
        }
        
        List<Approval.ProcessWorkitemRequest> testAppRejs 
                                                  = new List<Approval.ProcessWorkitemRequest>{};
        
        for (Integer i = 0; i < 50 ; i++)
        {
            Approval.ProcessWorkitemRequest testRejWithComment = new Approval.ProcessWorkitemRequest();
            testRejWithComment.setComments  ('Rejecting request with a comment.');
            testRejWithComment.setAction    ('Reject');
            testRejWithComment.setWorkitemId(reqResults[i*4].getNewWorkitemIds()[0]);
            
            testAppRejs.add(testRejWithComment);
            
            Approval.ProcessWorkitemRequest testRejWithoutComment = new Approval.ProcessWorkitemRequest();
            testRejWithoutComment.setAction    ('Reject');
            testRejWithoutComment.setWorkitemId(reqResults[(i*4)+1].getNewWorkitemIds()[0]);
            
            testAppRejs.add(testRejWithoutComment);
            
            Approval.ProcessWorkitemRequest testAppWithComment = new Approval.ProcessWorkitemRequest();
            testAppWithComment.setComments  ('Approving request with a comment.');
            testAppWithComment.setAction    ('Approve');
            testAppWithComment.setWorkitemId(reqResults[(i*4)+2].getNewWorkitemIds()[0]);
            
            testAppRejs.add(testAppWithComment);
            
            Approval.ProcessWorkitemRequest testAppWithoutComment = new Approval.ProcessWorkitemRequest();
            testAppWithoutComment.setAction    ('Approve');
            testAppWithoutComment.setWorkitemId(reqResults[(i*4)+3].getNewWorkitemIds()[0]);
            
            testAppRejs.add(testAppWithoutComment);            
        }
            
        Test.startTest();        
            // Process the approvals and rejections
            try
            {
                List<Approval.ProcessResult> testAppRejResults =  Approval.process(testAppRejs);
                system.assert(false, 'Any rejections without comments should cause an exception');
            }
            catch(DMLException e)
            {
                system.assertEquals(50, e.getNumDml());
                
                for(Integer i = 0; i < 50 ; i++)
                {
                    system.assertEquals((i*4) + 1, e.getDmlIndex(i));
                    system.assertEquals('Operation Cancelled: Please provide a rejection reason!', 
                                        e.getDmlMessage(i));
                }
            }    
        Test.stopTest();
    }
    
    /*
        Utility method for creating single object, and submitting for approval.
        
        The method should return the Id of the work item generated as a result of the submission.
    */
    private static Id generateAndSubmitObject()
    {
        // Create a sample invoice statement object and then submit it for approval.
        Invoice_Statement__c testIS = new Invoice_Statement__c();
        insert testIS;
        
        Approval.ProcessSubmitRequest testReq = new Approval.ProcessSubmitRequest();
        testReq.setObjectId(testIS.Id);
        Approval.ProcessResult reqResult = Approval.process(testReq);
        
        System.assert(reqResult.isSuccess(),'Unable to submit new invoice statement record for approval');
        
        return reqResult.getNewWorkitemIds()[0];
    }
}

Tuesday, 7 August 2012

Visualforce Component that creates a picklist without a -none- value

About a year and a half ago, when I was relatively new to the Force.com platform, I devised a way to create a picklist value selector with a '-none-' value for a field that could be placed on to a Visualforce page, to prevent users from not setting a value for a picklist field.

A question was posted in that blog entry that asked if it was possible to have two picklists for different fields on the same Visualforce page. I replied with an answer that was basically "write the code out twice". While this works, its not exactly re-usable or easy to maintain.

Being a big fan of Visualforce components, I set about converting the no '-none-' picklist code into a re-usable component that can be placed onto any number of pages, and used in different applications easily. I found that this was possible by using the describe object and describe field calls.

The component I have created is called InputPicklistNoNone. It has two attribute arguments that have to be defined when using it in a Visualforce Page:
  • Value: This is the reference for the variable you want to assign the picklist selection to. It is much like the value attribute for most Visaulforce input components, such as apex:inputField and apex:inputText.
  • Field: This is the API reference of the picklist field you want to extract the possible values from. If this value points to an invalid field, an error message will be added to the page.
Below is the component code, and sample page and controller code to show how the component can be used. This is just a simple example using the component in a Visualforce Page and a standard controller with an extension. The component is flexible, it can be used with custom controllers and the value it populates can be an object field or a string variable.

Component Code:
<apex:component controller="InputPicklistNoNoneController">
  <apex:attribute name="value" required="true" type="String" 
   description="The variable the selected option will be assigned to"/>
  
  <apex:attribute name="field" assignTo="{!fieldName}"    
   required="true" type="String" 
   description="The picklist field that forms the basis of the input"/>

  <!--If the value is empty, then set it to be equal to the default 
      value from the schema description-->

  <apex:variable var="value" value="{!defaultOption}" 
   rendered="{!ISNULL(value)}"/>

  <apex:selectList size="1" value="{!value}">
    <apex:selectOptions value="{!options}"/>
  </apex:selectList>
</apex:component>

Component Controller:
public class InputPicklistNoNoneController 
{
    public String             defaultOption {get;set;}
    public List<SelectOption> options       {get;set;}
    public String             fieldName
    { 
        get;
        set 
        {
            fieldName = value;
            options = new List<SelectOption>();
            List<String> fieldNameSplit = fieldName.split('\\.');
        
            Schema.DescribeFieldResult picklistFieldDescription =
              Schema.getGlobalDescribe().get(fieldNameSplit[0])
              .getDescribe().fields.getMap()
              .get(fieldNameSplit[1]).getDescribe();

            for (Schema.Picklistentry picklistEntry:
                 picklistFieldDescription.getPicklistValues())
            {
                options.add(new SelectOption(pickListEntry.getValue(),
                                             pickListEntry.getLabel()));

                if (picklistEntry.defaultValue)
                {
                    defaultOption = pickListEntry.getValue();
                }
            }    
        }
    }    
}

Example Page:
<apex:page standardController="Invoice_Statement__c"
           extensions="InputPicklistNoNoneExampleExtension">
           
  <apex:sectionHeader title="No none picklist component example"/>
  
  <apex:form >
    
    <apex:panelGrid columns="2">
    
      
      <!-- The component can be used to populate an object field -->  
      <apex:outputText value="Status:"/>
      <c:InputPicklistNoNone value="{!Invoice_Statement__c.Status__c}" 
                             field="Invoice_Statement__c.Status__c"/>
    
      
      <!-- The component can also be used with a controller variable -->
      <apex:outputText value="Industry:"/>
      <c:InputPicklistNoNone value="{!myControllerVariable}"
                             field="Account.Industry"/>    
    
      <apex:commandButton value="Save" action="{!save}"/>
    
    </apex:panelGrid>
    
  </apex:form>  
</apex:page>

Example Controller Extension:
public class InputPicklistNoNoneExampleExtension 
{
    public String myControllerVariable {get; set;}

    public InputPicklistNoNoneExampleExtension(
           ApexPages.StandardController controller) {}

}

Screenshot of Example Page:

I am a big fan of how this has turned out, it keeps the functionality intact while making it easily adaptable for use on numerous variables on any number of pages, result! If you have any questions, or need some pointers on how to use it, then please add a comment below.

Friday, 25 May 2012

Summer '12 release component layout attribute

*Special thanks to Abhinav Gupta on this one, without his blog post on his Summer'12 release favourites, this problem would have taken a lot longer to diagnose*

In the upcoming Summer '12 Salesforce release, one of the new features is a layout attribute for Apex component tags. This new layout feature allows you to wrap your components in a div or span tag. The full definition can be found in the release notes under "Visualforce Enhancements". It states that the default value for the new attribute is "inline" (span tag), so any existing component where this attribute does not exist should be unaffected, as is the Salesforce release way.

However, I have found in some cases that this new attribute has had an adverse affect on the appearance of Visualforce pages and email templates that leverage components.

For example, if you have an email template that that has a text attachment defined in Visualforce, and that definition has components included, span tags will now appear as part of the attachment. Take this simple email template and component definition.

Visualforce Email Template:
<messaging:emailTemplate subject="Sample Email With Text Attachment" 
                         recipientType="Contact" >

<messaging:plainTextEmailBody >
Wow look at this attachment, It's nearly as amazing as hoverboards!
</messaging:plainTextEmailBody>

<messaging:attachment renderAs="text/plain" filename="test.txt">
Hi {!recipient.Name}, Here is your awesome text!
<c:TextComponent />
</messaging:attachment>

</messaging:emailTemplate>


Visualforce Compnent Defintion:
<apex:component access="global">
Text from a component, YES! YES! YES!
</apex:component>

If an email message using this template is sent out this is how it appears in the recipient inbox if the Salesforce Org uses the Summer '12 release:



Here is the text attachment. As you can see it contains span tags, which is not what was intended. The same template would not have produced these tags in previous releases.



The way to resolve this is simple, just add the new layout attribute to the apex component definition, with a value of "none".
<apex:component access="global" layout="none">
Text from a component, YES! YES! YES!
</apex:component>

Now when an email using this template is sent, the email attachment looks like we want:


Summer '12 is currently in sandbox orgs, but will be promoted to all orgs over the next few weeks. My advice would be to identify the pages and templates where components are used before the release, making sure they still behave as intended. If they don't, add the layout attribute with a value of "none" as above.

Monday, 21 May 2012

FxFW - Force By Force West, A new Force.com developer group for the South West

Building on the highly successful "Force West" Salesforce networking community for the South West, a developer spin off community, "Force by Force West" (FxFW) is starting.

The aim of this new group is to share experiences and thoughts on all things Force.com over a pint or two, from Apex and Visualforce to mobile applications and alternative cloud platforms.

The first event will take place on the 31st May at the Elephant pub in Central Bristol, next to St. Nicholas Market.



View Larger Map


More information about the event and group can be found through the FxFW Facebook Page, or by following the FxFW Barman on Twitter.

If you are interested, let us know by registering on our Eventbrite entry, purely so we can get an idea of how many are coming down.

Hope to see you there!

Wednesday, 25 April 2012

Clone Plus: Clone Salesforce objects with children

In a previous post, I outlined an Apex cloning method that copied an sObject in its entirety, all data values included. Recently there have been some comments and questions on the post asking if it would be possible to use the method to create a way to clone an object and its children via a custom button on an object page without extensive development expertise required.

With that in mind, I have created a little helper tool I have called "Clone Plus" that does just that. It consists of an Apex controller, a single Visualforce page and custom button.

The clone plus button can be created on custom and standard object page layouts. When clicked, the button redirects the user to a Visualforce page that displays the children that can be cloned and associated with the new object. The user selects the objects they want to clone using checkboxes, and then clicks on a clone button to finish the operation. After the saving process is completed, the user is redirected to the new object.

The clone button on the page



The child clone selection screen



The new cloned object



Create the clone button by following the following steps: 

1) Create a new Apex class (Navigate to Setup -> Develop -> Apex Classes and click new). Copy in the following code into the class body:

public class ClonePlusController {

  public List<relatedObjects> objectChildren  { get; set; }
  public String               objectTypeName  { get; set; }
  public String               objectName      { get; set; }
   
  private SObject headSObject, headClone;
  
  // Initialisation method called when the clone plus page is loaded.
  // Use the id page parameter to find out what kind of 
  // object we are trying to clone.
  // Then load the object from the database.
  // Finally call the populateObjectChildren method to      
  public void initialiseObjectsForCloning()
  {

    // Here we generate a keyprefixmap using the global describe 
    // Then compare that to our object to determine type.  
    Map<String, Schema.SObjectType> gd = 
                   Schema.getGlobalDescribe(); 
       
    Map<String,String> keyPrefixMap = 
                  new Map<String,String>{};
          
    for(String sObj : gd.keySet()){
      Schema.DescribeSObjectResult r =  gd.get(sObj).getDescribe();
      keyPrefixMap.put(r.getKeyPrefix(), r.getName());
    }
      
    String objectID = 
           ApexPages.currentPage().getParameters().get('id');
    
    String objectTypeKey = 
           objectId.subString(0,3);
      
    objectTypeName = 
           keyPrefixMap.get(objectTypeKey);
      
    String primaryObjectQueryString = 'SELECT Id, Name FROM '
                                    + objectTypeName
                                    + ' WHERE Id = \''
                                    + objectId
                                    + '\'';
    
    headSObject = Database.query(primaryObjectQueryString);
    objectName          = '' + headSObject.get('Name');
    populateObjectChildren();    
  }

  // Get all of the children of the current object that have a 
  // object type contained in the child object types page parameter.
  // Not restricting the child objects to particular types 
  // results in unclonable system objects being added to the options, 
  // which we need to avoid (You will not want to clone these!)
  // Making these object type choices also allows us 
  // focus our efforts on the specific kinds of objects 
  // we want to allow users to clone.  
  public void populateObjectChildren()
  {
       
    objectChildren = new List<relatedObjects>{};
        
    Set<String> childObjectTypes = new Set<String>{};
    
    // read the object types from the page parameter.    
    childObjectTypes.addAll(
         ApexPages.currentPage().getParameters()
                                .get('childobjecttypes')
                                .split(',')
    );
    
    // Use the sobjecttype describe method to retrieve all 
    // child relationships for the object to be cloned.    
    Schema.DescribeSObjectResult headDescribe = 
           headsObject.getSObjectType().getDescribe();
    
    List<Schema.ChildRelationship> childRelationships = 
           headDescribe.getChildRelationships(); 
    
    // Iterate through each relationship, 
    // and retrieve the related objects.       
    for (Schema.ChildRelationship childRelationship : 
                                  childRelationships)
    {
      Schema.SObjectType childObjectType = 
                         childRelationship.getChildSObject();
      
      // Only retrieve the objects if their type is 
      // included in the page argument.          
      if (childObjectTypes.contains(
                           childObjectType.getDescribe().getName()))
      {
        List<relatedObjectRow> relatedObjects = 
                         new List<relatedObjectRow>{};
                
        Schema.SObjectField childObjectField = 
                         childRelationship.getField();
                
        String relatedChildSObjectsquery = 
               'SELECT ID, Name FROM ' 
             + childObjectType.getDescribe().getName()
             + ' WHERE '
             + childObjectField.getDescribe().getName()
             + ' = \'' 
             + headsObject.Id
             + '\''; 
                                                        
        for (SObject childObject : 
             Database.query(relatedChildSObjectsquery))
        {
          relatedObjects.add(new relatedObjectRow(childObject));
        }
            
        if (!relatedObjects.isEmpty())
        {
          objectChildren.add(new relatedObjects(relatedObjects, 
                childObjectType.getDescribe().getLabelPlural(), 
                childObjectField.getDescribe().getName()));
        }  
      }
    }
  }
  
  // Perform the cloning process.
  // First clone the parent, then all of the child objects. 
  // Then redirect the user to the new object page.
  public PageReference doClone()
  {
    headClone = cloneObjects(new List<sObject>{headSObject}).get(0);
    
    insert headClone;
    
    cloneSelectedObjects();
    
    return new PageReference('/' + headClone.Id);
  }
  
  // Clone the selected child objects.
  // Associate the cloned objects with the new cloned parent object.
  public void cloneSelectedObjects()
  {
        
    List<sObject> clonedObjects = new List<sObject>{};
    List<sObject> selectedRelatedObjects;
     
    for (relatedObjects relatedObject : objectChildren)
    {
      selectedRelatedObjects = new List<sObject>{};  
      clonedObjects = new List<sObject>{};  
      
      for (relatedObjectRow row : relatedObject.objectRows) 
      {
        if (row.selected)
        {
          selectedRelatedObjects.add(row.obj);
        }
      }
      
      if (!selectedRelatedObjects.isEmpty())
      {
        clonedObjects = cloneObjects(selectedRelatedObjects);
        
        for (sObject clone : clonedObjects)
        {
          clone.put(relatedObject.relatedFieldName, headClone.Id);  
        }
        
        insert clonedObjects;
      }
    }
  }

  // Clone a list of objects to a particular object type
  // Parameters 
  // - List<sObject> sObjects - the list of objects to be cloned 
  // The sObjects you pass in must include the ID field, 
  // and the object must exist already in the database, 
  // otherwise the method will not work.
  public static List<sObject> cloneObjects(List<sObject> sObjects){
                                                
    Schema.SObjectType objectType = sObjects.get(0).getSObjectType();
    
    // A list of IDs representing the objects to clone
    List<Id> sObjectIds = new List<Id>{};
    // A list of fields for the sObject being cloned
    List<String> sObjectFields = new List<String>{};
    // A list of new cloned sObjects
    List<sObject> clonedSObjects = new List<sObject>{};
    
    // Get all the fields from the selected object type using 
    // the get describe method on the object type.    
    if(objectType != null)
    {
      for (Schema.SObjectField objField : 
           objectType.getDescribe().fields.getMap().values())
      { 
        Schema.DescribeFieldResult fieldDesc = objField.getDescribe();
        // If the field type is location, then do not include it,
        // otherwise it will cause a soql exception.
        // Note that excluding the field does not stop the location from
        // being copied to the new cloned object.
        if(fieldDesc.getType() != DisplayType.LOCATION)
        {
          sObjectFields.add(fieldDesc.Name);
        }
      }
    }
    
    // If there are no objects sent into the method, 
    // then return an empty list
    if (sObjects != null || 
        sObjects.isEmpty() || 
        sObjectFields.isEmpty()){
    
      // Strip down the objects to just a list of Ids.
      for (sObject objectInstance: sObjects){
        sObjectIds.add(objectInstance.Id);
      }

      /* Using the list of sObject IDs and the object type, 
         we can construct a string based SOQL query 
         to retrieve the field values of all the objects.*/
    
      String allSObjectFieldsQuery = 'SELECT ' + sObjectFields.get(0); 
    
      for (Integer i=1 ; i < sObjectFields.size() ; i++){
        allSObjectFieldsQuery += ', ' + sObjectFields.get(i);
      }
    
      allSObjectFieldsQuery += ' FROM ' + 
                               objectType.getDescribe().getName() + 
                               ' WHERE ID IN (\'' + sObjectIds.get(0) + 
                               '\'';

      for (Integer i=1 ; i < sObjectIds.size() ; i++){
        allSObjectFieldsQuery += ', \'' + sObjectIds.get(i) + '\'';
      }
    
      allSObjectFieldsQuery += ')';
    
      system.debug('allSObjectFieldsQuery: ' + allSObjectFieldsQuery);
    
      try{
      
        // Execute the query. For every result returned, 
        // use the clone method on the generic sObject 
        // and add to the collection of cloned objects
        for (SObject sObjectFromDatabase:
             Database.query(allSObjectFieldsQuery)){
          clonedSObjects.add(sObjectFromDatabase.clone(false,true));  
        }
    
      } catch (exception e){
      }
      
    }
   
    return clonedSObjects;
    
  }
  
  // Related objects data construct - 
  // used to store a collection of child objects connected to 
  // the head object through the same relationship field.
  public class relatedObjects
  {
    public List<relatedObjectRow> objectRows { get; set; }
    public String                 pluralLabel      { get; set; }
    public String                 relatedFieldName { get; set; }
    
    public relatedObjects(List<relatedObjectRow> objectRows, 
                          String pluralLabel, 
                          String relatedFieldName) 
    {
      this.objectRows       = objectRows;
      this.pluralLabel      = pluralLabel;
      this.relatedFieldName = relatedFieldName;
    }   
  }     

  // An indidual child object row. 
  // Each instance simply contains the object definition, 
  // and a checkbox to select the row for cloning 
  // on the clone plus page.
  public class relatedObjectRow
  {
    public sObject obj      { get; set; }
    public Boolean selected { get; set; }
    
    public relatedObjectRow(Sobject obj)
    {
      this.obj      = obj;
      // All object rows are selected by default.
      this.selected     = true;
    }
    
    public String getName(){
      try{
        return '' + obj.get('Name');
      } catch (Exception e){
        return '';
      }    
    }   
  }
}

2) Create a new Visualforce Page  (Navigate to Setup -> Develop -> Pages and click new).
Enter ClonePlus in both the label and name fields.




3) Copy the following into the Visualforce Markup section:

<apex:page controller="ClonePlusController" action="{!initialiseObjectsForCloning}">
  
  <apex:sectionHeader title="Clone Plus: {!objectName}"/>

  <apex:form id="theform" >
  
    Please select the child objects you would like to clone.<br/><br/>
  
    <apex:repeat value="{!objectChildren}" var="child">
      <apex:PageBlock title="{!child.pluralLabel}"> 
        <apex:pageBlockTable value="{!child.objectRows}" 
                             var="objectRow">
          <apex:column headerValue="Clone" width="10%">
            <apex:inputCheckbox value="{!objectRow.selected}"/>
          </apex:column>
          <apex:column headerValue="Name" value="{!objectRow.name}" 
                                          width="90%"/>
        </apex:pageBlockTable>
      </apex:PageBlock>
    </apex:repeat>
    
    <apex:PageBlock >
      <apex:commandButton action="{!doClone}" value="Clone"/>
    </apex:PageBlock>
  </apex:form>  

</apex:page>

4) Navigate to the custom button and links menu for the object you want to add the clone with children functionality for.

If this is standard object, select  Setup -> Customize -> *Object Name* -> Buttons and Links. See the following example for contact.



If instead you want to clone a custom object, go to the custom objects definition page (Setup -> Create Object) and select the object from the list. Once the custom object definition page is open, scroll down to see the button and links menu. The following screenshots show how to do this for an example object called Invoice Statement




5) Click on the new button on the menu. Populate the form values as follows:




6) In the button definition body, write a URL in the following form:

/apex/ClonePlus?id={!*Parent Object Type*.Id}&childobjecttypes=*Child Object Types*

Where  *Parent Object Type*  is the API name of the object you are creating the button for, and  *Child Object Types* is a comma separated list of the types of child object you want to be able to clone alongside your main parent object.

Here are some examples to help:

If you want to clone the standard Account object from the accont page, but also want to clone related standard Contact objects at the same time:

/apex/ClonePlus?id={!Account.Id}&childobjecttypes=Contact

If you want to clone the standard Campaign object, and also want the option to clone related standard Opportunity objects, but also related child instances of a custom object called Campaign Advert (Api name: Campaign_Advert__c):

/apex/ClonePlus?id={!Campaign.Id}&childobjecttypes=Opportunity,Campaign_Advert__c

If you want to clone a custom object of type Custom Parent (Api name: Custom_Parent__c) and two related child custom object types Custom Child (Api name:  Custom_Child__c) and Another Child (Api name:  Another_Child__c):

/apex/ClonePlus?id={!Custom_Parent__c.Id}&childobjecttypes=Custom_Child__c,Another_Child__c

Your form should look something like this:



Click the save to confirm the changes.

NOTE: If you have any problems defining your URL, write a comment below with the object names and relationships and I would be happy to reply with what the value should be.

7) Finally, add the new "Clone Plus" custom button to the page layouts you want it to appear on, using the handy WYSIWYG editior.

Page Layouts for standard objects can be found through the menu ( Setup -> Customize -> *Object Name* -> Page Layouts)



For custom objects, the page layouts appear on the same object summary page as the custom button and links menu.

So there you have it, Happy Cloning!! This is a first iteration of Clone Plus. I hope to develop this concept as time moves forward, all feedback and suggestions is greatly appreciated!

A few notes:
  • Due to the generic nature of the method, you can add this functionality to as many objects as you like. You do not need to create any extra controllers or pages, they can all use the two already defined.
  • At the moment clone plus only clones direct children, not parents, grandchildren or further relationships. Of course this is possible, all it takes is some more complex method calls. For this example I wanted to keep things simple.
  • In this example I make the user define the types of object they want to clone. You may be asking, why not just clone everything? This was actually the approach I had at the beginning, but after trying it out, there are a lot of setup objects that are related to each object type behind the scenes. Most of these related objects would never be cloned, so including them does nothing but confuse the user.


Tuesday, 10 April 2012

Forcewest - 17th April 2012



Calling all Salesforce professionals in the south west of England! The third Forcewest meeting will be taking place on the 17th April 2012 6pm at Totos wine bar, Bristol.


Forcewest is a unique Salesforce community group organised by Desynit that brings together local Salesforce and cloud computing professionals based in Bristol and the surrounding area. 


In this Forcewest, Simon Parker will be presenting a Salesforce implementation case study for the charity Surf Life Saving. After the presentation, Simon will be holding a Q&A session. The community will then be invited to network, exchange experiences and discuss all things Salesforce.


So if your a Salesforce developer, administrator or user, or just curious about what the local cloud community has to offer, please register your name on our Eventbrite page 


Oh and one last thing.... Desynit will be placing a tab behind the bar, so come on down and grab a free pint :D . 

Tuesday, 14 February 2012

10 Easy steps to an improved default object home page

Around a year and a half ago, I wrote this rant of a post, outlining my frustrations with the default object home view pages. I am not alone in my frustration, an entry on the Salesforce ideas site to address this problem is proving quite popular. Given the apparent demand for an alternative, I started to look at the default landing page again and think about ways to improve it.



The common complaint concerning the page is that is has a very counter intuitive feel to it. When the page is loaded, our eyes are drawn straight away to view selection box in the top left hand corner and the list of objects directly below it. Where the confusion starts is that the selected view has no bearing on the displayed objects. The list of objects is the collection of items the user has recently viewed, edited or created, controlled by the tiny combo box on the far right hand side of the page.

The only way to access objects in the form of a view is to click the "Go" button by the view combo box. This page, as seen below, shows the data in a tabular form that is fully editable, where the sort order can be varied easily through clicking on the column headers. The view of the data can be changed easily through the same combo box as on the main screen.

After seeing the views page, I reasoned that I would much prefer this page as a home page for my objects than the standard view. There are no conflicting page elements, and I am not forced to see a recent items list, instead I have complete control over the view of the data.


After this realisation, I attempted to build a new homepage based on the tabular data summary that could be used for both standard and custom objects.

This actually turned out to be a lot simpler than anticipated, placing the enhanced list Visualforce component in a page provides a simple way to build view pages for an object. My only addition was a link to the default object page, just in case I ever required a recent view or one of the extended tool links. Assign this page to a tab, replace the original tab, and the user experience is much improved.

To set up a alternative default tab for a standard or custom object, simply follow these steps:

1) Open the current standard tab for your object, and note down the three characters at the mid section of the URL in the navigation bar of your internet browser. For example, Contact is '003' and Lead is '00Q'. I am going to create an alternative standard tab for the Account object, so the object code is '001'.



2) Create a new Visualforce page, by navigating to (Setup -> App Setup -> Develop -> Pages) and clicking on the New button. Call your new page "Alternative" + object label + "Homepage", such as "AlternativeContactHomepage" or "AlternativeMyCustomObjectHomepage". In the account example this would be "AlternativeAccountHomepage".


3) Erase the default body content created automatically by Salesforce, copy the following Visualforce code and place it in the body definition entry section. Don't save the page yet, a few attribute values within the tags need to be populated first.

<apex:page>
  <apex:sectionHeader title=""/>
  <apex:enhancedList type="" height="600" rowsPerPage="50"/>
  <br/>
  <apex:outputLink value="//o">
    See Recent Items / Extra Options
  </apex:outputLink>
</apex:page>


4) Inside the apex section header tag, enter the plural version of the object name. Enter the API name of the object in the type attribute of the apex enhanced list tag. Finally, place the object three character code recorded during step 1 in between the two forward slash characters in the value attribute of the apex output link.

For a standard object, such as Account, this page code should look like this:

<apex:page >
  <apex:sectionHeader title="Accounts"/>
  <apex:enhancedList type="Account" height="600" rowsPerPage="50"/>
  <br/>
  <apex:outputLink value="/001/o">
    See Recent Items / Extra Options
  </apex:outputLink>
</apex:page>

For a custom object, such as My Custom Object (with a three digit code of a01), the page code should look like this:

<apex:page >
  <apex:sectionHeader title="My Custom Objects"/>
  <apex:enhancedList type="My_Custom_Object__c" height="600" rowsPerPage="50"/>
  <br/>
  <apex:outputLink value="/a01/o">
    See Recent Items / Extra Options
  </apex:outputLink>
</apex:page>

To finish, click the Save button to save your new page.



5) Navigate to the tabs definition page (Setup -> App Setup -> Create -> Tabs). Click on the New button in the Visualforce Tabs section. Place the object name as it appears on the existing default object tab (i.e. Contacts or My Custom Objects). In the API name, enter the same. Choose a tab style (may I personally recommend Big Top or Saxophone :-D ), then click Next.



6) On the next page, select the users you want to provide the new tab view to, and click Next.


7) On the final page, select the applications that you want to add the alternative tab to (probably where the standard tab exists currently). Click Save to finish.



8) The new tab should now be visible in the apps you have selected. However, you may have noticed that the standard tab is still visible in your applications. As you can see below, I still have the Accounts standard tab, and the Accounts alternative tab visible, both called "Accounts".



The standard tabs need to be removed for clarity. To do this, navigate to the Apps setup page (Setup -> App Setup -> Create -> Apps). Click the edit link next to one of the apps that contains both of the tabs.



9) On the edit screen, Under the tab selection panel, move the old standard tab from the right hand side column to the left. Move the new alternative tab (at the end of the list) up the tab order to replace the old standard tab. Click Save to confirm your changes.





10) Finally, Click on your new tab to view your new object home page!


The new object home page is largely the same as the view page found by clicking the "Go" button on the original screen. You can use this interface to easily navigate through large amounts of objects. The current view of the data can be simply altered using the combo box in the top left hand corner of the page. You can order the view by different fields, and use the A-Z links in the top right hand corner to skip to specific alphabetic subsets of your data.


At the bottom of the page, you will find a link back to the old home page. This is useful in case you actually decide once in a while that you want to see your recent items or follow one of the default links for a standard object.

So there you have it, a much improved standard landing page for all of your objects. This is still far from ideal I admit, but I believe it is a definite step in the right direction. Most importantly I feel using this kind of view is intuitive. There is no longer all the confusion of recent views, especially debilitating for new users.

If you have any problems, questions or improvement suggestions, please add a comment below... Happy tabbing!


NOTE: If you want to remove the standard tab from being accessed entirely by a set of users, simply navigate the user profile settings (Setup -> Administration Setup -> Manage Users -> Profiles). Click the edit link next to a profiles you want to alter. Find the section where standard tab access is defined, and in the combo box next to the standard object tab, select the "tab hidden" option. Any user with this profile will now not be able to view the old standard tab, only the new alternative tab.

Standard Profile User Interface:



Enhanced Profile User Interface:



Sunday, 6 November 2011

How to create a "Past 24 hours" flexible report

Sometimes as a Force.com developer you can be asked to complete tasks that on the surface seem simple, but can quickly prove beyond difficult due to the current limitations of the platform. Recently a client asked if it was possible to construct a report that listed all the Accounts that had been created in the past twenty four hours. They were looking to run this report at the end of their business day, which runs between 3pm one day to 3pm the following day.

Given this report criteria, lets take a look at the options available at a day level in reporting for a datetime field like created time:



As you can see, we are limited to specific datetime ranges (Current day, yesterday, this week, last FY etc.). While there are standard criteria values relating to the current day (00:00:00 to 23:59:59), there are no entries that relate to the current time in the system. Of course this time range can be defined manually in the "From" and "To" entry fields when using the "Custom" range option, but this means the report will only work for that specific time frame, essentially meaning a new unique report has to be created each day.

At this point, my options seemed to be:
  1. Instruct the client to change the report criteria daily and run the report manually at 3pm (unreliable and time consuming).
  2. Try to convince the client to change their business practice hours to fit the strict day by day Salesforce model (and find myself in an Force.com developer shaped hole in the wall).
Not wanting to propose either of these approaches, I pressed on, determined to find another way. I came to the realisation that while it is not possible to create a 24 hours report directly through manipulating criteria in the report builder, a custom formula field can be created on the account object that can identify if the created date field date/time value is within the last the past 24 hours. A report can then be created where results are filtered based on the value of this formula field.

To do this, first create a new text formula field on the Account object, giving it a suitable name such as "Account created past 24 hours". In the formula entry panel, enter the following formula:

IF ( NOW() - CreatedDate < 1 ,"TRUE", "FALSE" )

In formula fields, if you subtract one date/time from another, the result is returned in the form of a number. This number is the amount of days between the two expressed as a decimal (1 = 1 day, 3.5 = 3 days 12 hrs etc). The formula above calculates the decimal time difference between the current system time and the date and time the account object was created. If this value is less than or equal to 1, it follows that the account was created in the last 24 hours.

Formula fields cannot be defined as returning a simple boolean value, so instead the formula is set as a text field and the logic condition is encapsulated inside an if statement. If the condition is true (account created in past 24 hours) a TRUE text value will be returned, if the condition is false, FALSE will be returned. If preferred, this could be simplified to Y and N, or even a numerical formula field with values of 1 and 0.

After creating the formula field, include the field and the created date in an account view to ensure the field behaves as intended. The following screenshot was created today. Don't be fooled by the appearance of the created date field. Although it only displays a date string with no time, it is a date/time field. To see the date time value, simply click onto an account record and find the field on the account detail page.  



We can now leverage this formula field to create our past twenty four hours report. Simply create a new account report, and add a new filter where the value of our custom "Account created past 24 hours" formula field is equal to "TRUE". Make sure that the Range date criteria on the line above is set to "All Time"



Use the report builder to select all the detail fields you wish to include in the report, and save the new report.  Run the report, and you should see the details of all the accounts created in the past 24 hours. The client now has a report they can run at three o'clock that shows all the accounts created in the past 24 hours. Magic!



BUT!!!
This is still not an entirely suitable solution for our problem. The original request was for a report that could see the accounts that were created between 3pm on the current day and 3pm the previous day. If the client ran the report at any time other than exactly 3pm, the result would be skewed, and relevant account records could be potentially excluded from the report.

If this report were to be scheduled to be run every day at 3pm inside Salesforce, the same skewing would occur, as report scheduling simply indicates a preferred time. The following is extracted directly from Salesforce scheduling a report documentation
  • The report runs within 30 minutes of the time you select for Preferred Start Time. For example, if you select 2:00 PM as your preferred start time, the report runs any time in between 2:00 PM and 2:29 PM, depending on how many other reports are scheduled at that time.

The formula needs to be adapted to cope with this potential skewing, so the report can be scheduled to run correctly. Instead of just comparing the created date field to the value of the current system date time (NOW()), the current system time needs to be rounded to last whole hour. There is no method or function within formula fields that allows for the manipulation of date/time fields, so in order to accomplish this we need to transform the two date time variables into a numerical forms that can be manipulated.

As illustrated in the initial draft of the formula above, the result of subtracting one date time value from another is always returned in a numerical form. Comparable numerical values of our date time variables can therefore be created by subtracting a common early base date/time value from both values. Fortunately, Salesforce provides a suitable offset date/time out of the box. The System.OriginDateTime variable is a global Salesforce variable, and is a date/time representation of the turn of the 20th century (1900-01-01 00:00:00).

So to convert the current system date time into a numerical form, use the following inside a formula field:
NOW() - ($System.OriginDateTime)

and to convert the account created date to a similar comparable value:
CreatedDate - ($System.OriginDateTime)

However we are still not finished, we need to round the numerical representation of the current system time to the nearest hour. Remember that all numerical representations of date/time comparison values represent the number of days between the two time-stamps as a decimal. To convert this value to be in hours instead of days, simply multiply the decimal value by 24. The date/time value can now be rounded down to the nearest hour by using the FLOOR function on the decimal number. The excess minutes have now been removed. Divide the resultant rounded number by 24 to convert the value back into a value based on days, and it can now effectively be used in the comparisson formula.

The final formula looks like this:

IF ((FLOOR ((NOW() - ($System.OriginDateTime)) * 24) / 24)  - 
(CreatedDate - ($System.OriginDateTime)) < 1, "TRUE", "FALSE" )

If you update your formula field to the new value shown above, and then run the report again, you will see that it the cut off point for account created dates has now been rounded to the last hour. So it doesn't matter if you run the report at 3:00 pm, 3:01 pm, 3:30 pm or 3:59pm, the result will always be the same.

So now the client has a report they can run  that shows them the account records created in the last twenty four hours, that only varies by the hour, not the minutes. The report is scheduled so every day at some time between 3pm and 3:30pm they receive a direct email report of all accounts created in their last business day.

So there you have it, happy reporting! This was a task that seemed simple on the surface, but quickly became very complex. An effective solution has been created by using alternative thinking and manipulation of other native Salesforce utilities. I am a firm believer that although Salesforce does not always provide functionality as required straight out of the box, there is always another way to accomplish business goals with the suite of tools they provide.

A big plus point of this solution is that it opens to door to solve numerous report time criteria challenges. This date/time manipulation formula field could be applied to any salesforce date/time field, not just created date and not just on the account object. The formula can also be adjusted to different timeframe scenarios (previous 48 hrs, past ten minutes, an hour either side).

As a bonus tip if you ever have to use this method, there can be scenarios where you have to account for future time values. In the created date example, all values of the comparison field ,created date, were guaranteed to be in the past. If you had date/time values you wished to analyse that potentially had values in the future, you have to add an extra clause to account for the negative values, like so:

IF ( 
AND( 
Custom_DateTime_Field__c - ($System.OriginDateTime) >=
((FLOOR ((NOW() - ($System.OriginDateTime)) * 24) / 24) -1), 
Custom_DateTime_Field__c - ($System.OriginDateTime) <
(FLOOR ((NOW() - ($System.OriginDateTime)) * 24) / 24) 
), "TRUE", "FALSE" .
)

If you have any questions about the formula field, or how to define a formula field for a particular timespan or scenario, get in touch!

Sunday, 21 August 2011

Writing unit tests for the PageReference Class

Before you can deploy any Salesforce project to production, unit tests have to be written that ensure that future releases or platform upgrades do not cause any element of your system to malfunction. When writing tests for Apex controller classes, it is likely that you will have to write test methods that relate to action methods. Action methods in controllers have to return a Page Reference, which can be used to direct the user to another page as soon as the action completes. Parameters and other variables can also be added to the page references.

To achieve a suitable level of coverage and fault tolerance, your test methods should ensure that calling an action returns the correct page reference. It sounds simple enough, but testing these page references is not as straightforward as you might think. Take the following example of a simple controller method and an accompanying test method.

public class PageReferenceTestingController{

  PageReference changeThePageAction(){
    
   // Action code HERE

    return Page.SomeOtherPage;
  }
  
  static testmethod void testChangeThePageAction(){
  
    PageReferenceTestingController testPRTC = 
      new PageReferenceTestingController();
  
    system.assertEquals(Page.SomeOtherPage, 
                        testPRTC.changeThePageAction());
  }
}

Looks simple enough, but when we execute the test methods through the Salesforce interface, we retrieve the following error message:


Not really the most helpful message in the world, as the two are seemingly identical :) .

 The simplest way to avoid this problem is to simply compare the URL attributes of the page references, like so:

public class PageReferenceTestingController{

  PageReference changeThePageAction(){
  
    // Action code HERE
  
    return Page.SomeOtherPage;
  }
  
  static testmethod void testChangeThePageAction(){
  
    PageReferenceTestingController testPRTC = 
      new PageReferenceTestingController();
  
    system.assertEquals(Page.SomeOtherPage.getURL(), 
                        testPRTC.changeThePageAction().getURL());
  }
}

If we run the test methods for this class again, it successfully passes.

BUT......... we are only testing the URL of the redirected page, what if we have added parameters we want to check? Wouldn't it be better to test the whole object?

 The way to best achieve this is to write a static page reference comparison method. This will iterate through the different page reference attributes and compare them one by one. This will allow you to easily compare two page references quickly and effectively. See the following example.

ublic class PageReferenceTestingController{

  PageReference changeThePageAction(){
  
    // Action code HERE
  
    PageReference goToSomeOtherPage = Page.SomeOtherPage;
    
    goToSomeOtherPage.setRedirect(false);
    goToSomeOtherPage.getParameters().put('a','b');
    goToSomeOtherPage.getParameters().put('c','d');
    goToSomeOtherPage.getParameters().put('e','f');
  
    return goToSomeOtherPage;
  }
  
  static testmethod void testChangeThePageAction(){
  
    PageReferenceTestingController testPRTC = 
      new PageReferenceTestingController();
  
    PageReference testPageReference = Page.SomeOtherPage;
    
    testPageReference.setRedirect(false);
    testPageReference.getParameters().put('e','f');
    testPageReference.getParameters().put('a','b');
    testPageReference.getParameters().put('c','d');
  
    system.assert(arePageReferencesEqual(testPageReference, 
                                         testPRTC.changeThePageAction()));
  }
  
  static Boolean arePageReferencesEqual(PageReference page1, 
                                        PageReference page2){
  
    // First do a null test.
    if (page1 == null && page2 == null) return true;
    if (page1 == null && page2 != null) return false;
    if (page1 != null && page2 == null) return false;   
  
    // If none of the page references are null, compare their attributes.
    if (page1.getAnchor()     == page2.getAnchor()
     && page1.getURL()        == page2.getURL()
     && page1.getRedirect()   == page2.getRedirect()
     && page1.getCookies()    == page2.getCookies()
     && page1.getHeaders()    == page2.getHeaders()
     && page1.getParameters() == page2.getParameters()){
     
       return true;
    }   
      
    return false; 
  }
}

Friday, 15 July 2011

How to handle exceptions when using the Messaging.sendEmail Apex method

Apex, the native Salesforce OO language, provides methods to create and send emails. You can define your own body, or create standard templates using Visualforce, the Salesforce page markup language. Messages can be sent using the Messaging.sendEmail method. If any problems occur while attempting to send these emails, exceptions are thrown by the method.

Let's have a look at the method definition, taken directly from the Salesforce documentation.

Messaging.sendEmail(new Messaging.Email[] { mail } , opt_allOrNone);

The documentation explains that the all_or_none argument allows you to choose to send successful emails even if some fail. This sounds pretty straightforward, but what the documentation doesn't explain is that the value of this argument actually affects how errors are reported back to the calling program.

If the argument value is "true" or simply not populated, then all exceptions are thrown, and can be caught, reported and dealt with using a traditional try/catch structure.

    try{
      Messaging.sendEmailResult[] sendEmailResults = 
        Messaging.sendEmail(new Messaging.Email[] { mail });
    } catch(System.EmailException ex){
      // Exceptions are passed to here.
    }

However, if the value is set to "false" none of the exceptions are thrown. These are instead stored inside the Messaging.SendEmailResult collection that is passed back from the method call.

    
   Messaging.sendEmailResult[] sendEmailResults = 
      Messaging.sendEmail(new Messaging.Email[] { mail }, false);

   for(Messaging.SendEmailResult sendEmailResult: 
        sendEmailResults){
            
     if(!sendEmailResult.isSuccess()){
        // deal with failure here.
     }
   }
 

The following page and controller combo includes actions that allow you to directly see how calling the Messaging.sendEmail method with different argument values affects error handling. A sample combination of successful and failing emails are generated and passed to the method for testing.

Page:
<apex:page controller="EmailErrorHandlingExampleController">

    <apex:sectionHeader title="Email Error Handling Example"/>
    
    <apex:form >
        <apex:commandButton 
          action="{!sendSampleEmailsAllOrNothing}" 
          value="Send emails all or nothing"/>
        <apex:commandButton 
          action="{!handleIndividualSendEmailErrors}" 
          value="Send emails individually"/>
    </apex:form>    
    
    <apex:pageMessages />
    
</apex:page>

Controller:
public class EmailErrorHandlingExampleController {

  // Send some emails all or nothing, if one fails, all fail.
  public PageReference sendSampleEmailsAllOrNothing(){
    
    List<Messaging.SendEmailResult> sendEmailResults = 
      new List<Messaging.SendEmailResult>{};
    
    Integer numberOfSuccessfulMessages = 0;
    
    try{
      // note the all_or_none option has not been set 
      //(default value is true)
      // an email exception should be thrown, 
      // and the results list should not be populated. 
      sendEmailResults = 
        
        Messaging.sendEmail(getSampleEmailMessages());
     
    } catch(System.EmailException ex){
      // This message should be added to the page, 
      // as an exception is caught
      Apexpages.addMessage(new ApexPages.Message(
        ApexPages.Severity.ERROR, 
        'Caught email exception: ' + ex));
    }
        
    // This section of code should be skipped, 
    // as the result set is empty.
    for(Messaging.SendEmailResult sendEmailResult: 
        sendEmailResults){
            
      if(sendEmailResult.isSuccess()){
        numberOfSuccessfulMessages++;
      }
      else {
        for (Messaging.Sendemailerror sendEmailError : 
             sendEmailResult.getErrors()){
          
          Apexpages.addMessage(new ApexPages.Message (
            ApexPages.Severity.ERROR, 
            'Send Email Result Error: ' + 
            sendEmailError.Message));
        }
      }
    }
        
    // This page messsage should confirm 
    // no messages have been sent.
    Apexpages.addMessage(new ApexPages.Message (
      ApexPages.Severity.INFO, 
      'You should have just received ' 
      + numberOfSuccessfulMessages + ' emails'));
    
    return null;
  }
    
  // Send some emails on an individual basis. 
  // If one fails, only that one fails.
  public PageReference handleIndividualSendEmailErrors(){
        
    List<Messaging.SendEmailResult> sendEmailResults = 
      new List<Messaging.SendEmailResult>{};
    
    Integer numberOfSuccessfulMessages = 0;
    
    try{
      // note the all_or_none option has been set to false
      // an email exception should not be thrown, 
      // but rather stored in the result.
      sendEmailResults = 
        Messaging.sendEmail(getSampleEmailMessages(),false);
    
    } catch(System.EmailException ex){
      // This section should never be accessed, 
      // as an exception is never thrown.
      Apexpages.addMessage(new ApexPages.Message (
        ApexPages.Severity.ERROR, 
        'Caught email exception: ' + ex));
    }
        
    // This section of code should be run through four times, 
    // one for each populated result.
    for(Messaging.SendEmailResult sendEmailResult: 
        sendEmailResults){
            
      if(sendEmailResult.isSuccess()){
        numberOfSuccessfulMessages++;
      }
      else {
        for (Messaging.Sendemailerror sendEmailError : 
             sendEmailResult.getErrors()){
              
          Apexpages.addMessage(new ApexPages.Message (
            ApexPages.Severity.ERROR, 
            'Send Email Result Error: ' 
            + sendEmailError.Message));
        }
      }
    }
        
    // This message should indicate that 
    // two messages were successfully sent.
    Apexpages.addMessage(new ApexPages.Message (
      ApexPages.Severity.INFO, 
      'You should have just received ' + 
      numberOfSuccessfulMessages + ' emails'));
      
    return null;
  }

  // Get the current users emailAddress
  String currentUserEmailAddress {
        
    get{
      if (currentUserEmailAddress == null){
        
        // If the value has not been populated, retrieve it
        // using a SOQL query and the userinfo object.
        User currentUser = [SELECT email
                            FROM user
                            WHERE Id = :userInfo.getUserId()
                            LIMIT 1];
                                    
          currentUserEmailAddress = currentUser.email;
      }
        
      return currentUserEmailAddress;
    }
      
    set;
  }   

  // Generate a sample set of emails, 
  // some with errors, some without
  List<Messaging.SingleEmailMessage> getSampleEmailMessages(){
        
    // Simple success email message sent to the current user.    
    Messaging.SingleEmailMessage sampleSuccessEmail1 =
      new Messaging.SingleEmailMessage();
        
    sampleSuccessEmail1.setToAddresses(
      new List<String>{currentUserEmailAddress});
    
    sampleSuccessEmail1.setSubject('Just to let you know');
    
    sampleSuccessEmail1.setPlainTextBody(
      'This is a successful test email');
        
    // Simple fail message sent to the current user.    
    Messaging.SingleEmailMessage sampleFailEmail1 = 
      new Messaging.SingleEmailMessage();
        
    sampleFailEmail1.setToAddresses(
      new List<String>{currentUserEmailAddress});
    
    sampleFailEmail1.setSubject(
      'This is a failed email, it has no body!');
        
    // Simple fail message sent to no one.    
    Messaging.SingleEmailMessage sampleFailEmail2 =
      new Messaging.SingleEmailMessage();
        
    sampleFailEmail2.setToAddresses(new List<String>{});
    
    sampleFailEmail2.setSubject('This is a failed email');
   
    sampleFailEmail2.setPlainTextBody(
      'It is adressed to no one!');
        
    // Another Simple success email message 
    // sent to the current user.    
    Messaging.SingleEmailMessage sampleSuccessEmail2 = 
      new Messaging.SingleEmailMessage();
        
    sampleSuccessEmail2.setToAddresses(
    
      new List<String>{currentUserEmailAddress});
      
    sampleSuccessEmail2.setSubject(
      'Just to let you know (again)');
      
    sampleSuccessEmail2.setPlainTextBody(
      'This is another successful email');
        
    return new List<Messaging.SingleEmailMessage>{
      sampleSuccessEmail1,
      sampleFailEmail1,
      sampleFailEmail2,
      sampleSuccessEmail2};
  }
}

The following page messages should appear on the screen when you click the "Send emails all or nothing" command button:



Whereas these page messages should appear when the "Send emails individually" command button is clicked:



Being aware of the impact of the all_or_none argument on exception handling will help you to manage errors more effectively, and avoid writing redundant error handling code that will never be called.