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.

Wednesday, 18 May 2011

How to dynamically clone Salesforce objects with all fields populated


UPDATE 25/04/2012: If cloning child objects using a custom button is of interest to you, check out my post regarding Clone Plus!!!  

There are several scenarios during development where it becomes necessary to create complete copies of instances of Salesforce objects. Salesforce already provides a generic "clone" method to copy objects by casting them first to a more primitive base sObject. The clone method for sObjects even has a parameter which re-creates all relationships opt_IsDeepClone. The documentation describes that turning this option to true will result in a full copy of the sObject being returned by the method.

However, a common problem developers have when using this method is that it does not automatically copy all of the objects field values from the database. Instead, the opt_IsDeepClone option only copies the field values currently held in memory that have been retrieved through use of a SOQL query or have been populated directly in the Apex code. 

It is well known that Salesforce does not provide a "SELECT * FROM" method, so creating a cloning method that is guaranteed to copy all fields, and maintaining that functionality if any modifications are later made to the object, is challenging.

The way to accomplish a complete clone including all field values is to make use of the schema object definitions, which are accessible through apex code. You can use these definitions to retrieve all the fields for a particular object. These field name strings can be concatenated into a SOQL query to ensure all values are retrieved. My example code can be seen below:

public class SObjectAllFieldCloner {

  // Clone a list of objects to a particular object type
  // Parameters 
  // - List<sObject> sObjects - the list of objects to be cloned 
  // - Schema.SobjectType objectType - the type of object 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){
    
    // 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){
      sObjectFields.addAll(
        objectType.getDescribe().fields.getMap().keySet());
    }
    
    // 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 += ')';
    
      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){
        // Write exception capture method 
        // relevant to your organisation. 
        // Debug message, Apex page message or 
        // generated email are all recommended options.
      }
    }    
    
    // return the cloned sObject collection.
    return clonedSObjects;
  }
}

An example of how you would call this method  to clone an Account object from another Apex controller is:

Account originalAccount = [select Id from Account where 
                                name = 'My Favourite Account'];

sObject originalSObject = (sObject) originalAccount;

List<sObject> originalSObjects = new List<sObject>{originalSObject};
      
List<sObject> clonedSObjects = SObjectAllFieldCloner.cloneObjects(
                                      originalSobjects,
                                      originalSobject.getsObjectType());
                                     
Account clonedAccount = (Account)clonedSObjects.get(0);

Notes: This query only copies a single type of object at a time, related child objects are not cloned, however all lookup relationships/master record references are copied. Also in this example, I have shown how to create a virtual "SELECT * FROM" to make this resistant to change. This type of query should only be used when absolutely necessary, never out of developer laziness.

There are a lot of combined concepts in this example; mapping objects, using object definitions and generic sObject methods amongst other. If you require any further explanation to any of the elements of this solution, please list a question below.

Monday, 4 April 2011

Top 5 tips for passing the Salesforce.com certified Force.com developer (SP11) exam

I recently passed the Salesforce.com certified Force.com developer (SP11) exam (hence the shiny new icon on the right). The test covers a variety of material relating how to build applications using the platform. If you are preparing for the exam, here are my top 5 tips for successfully passing.
  1. This is not a coding exam

    The purpose of the developers exam is to measure how effectively you can describe how to use the various aspects of the platform to construct applications, not to prove how well you can code Visualforce pages or Apex classes. It is more important to be able to define when to use these tools, rather than how.

  2. Go back to basics

    If like me, you have been working primarily on providing specific customization to applications for a while, it is a good idea to re-reference all the components available on the Force.com platform. Part of my preparation for the exam included building the recruitment application as described in Force.com Fundamentals, which really helped to re-enforce how to use every aspect of the platform.  

  3. Learn to share

    A substantial proportion of the questions in the test are centered around how to enable and restrict access to objects and records. Learn the different methods of sharing and appreciate in what kind of scenarios these should be implemented. You should understand the whole range, from object level CRUD permissions using profiles to sharing records using public groups and manual rules. Also, in a similar vein, you should be aware of the relationship that page layouts have with record sharing.

  4. Know the optional extras

    Aside from the standard Force.com platform functionality, there are several additional features that can be included in an organization by contacting Salesforce. Examples of such features are encrypted fields to encrypt data shown to particular users and enabling the use of multiple currencies. Fortunately, all you need to know about these extras is included in the Study Guide.

  5. A report is more than just a table of data

    The reporting tool included on the force.com platform is highly customizable, and you will need to be aware of how you can adapt reports to a specific purpose. It is essential to learn the various report types, and given a situation which one you would use. The same is also true of dashboard components, understand what each is used for and recognize when it is appropriate to use them. When revising the use of reports, rather than just read, I think the best approach is to get hands on. If you have a developer force account, construct several reports and dashboards and really experiment with the various ways of presenting data to users. 

I wish you the best of luck!

Tuesday, 22 March 2011

Working with Apex data tables: Confirming choices

In my previous post, I used Apex page messages to provide direct feedback from an action to users. If the action you are about to perform is particularly critical or hard to reverse, you may want to add some kind of confirmation mechanism. Of course we could simply use a javascript function on the page to produce a simple confirm pop up, but it is usually preferable to provide something more comprehensive. An effective way to do this in Salesforce is to create a two page Visualforce menu consisting of an action page and a confirmation page, where we can make sure the user is aware of the action they are about to perform.

I have built an example of such a menu based on the page and controller from my previous post. When the user selects contact records and clicks on "process selected contacts", they are taken to a confirmation page that displays the contacts that they have selected, and asks them to confirm that these are the records they wish to process. This confirmation page shares the same controller as the main selection page. They can choose to cancel, which will return them to the previous selection menu taking no action, or click confirm to accept the selections and then be returned to the selection menu. Here is a screenshot of the confirmation screen:



In terms of altering the controller code, I added two new actions to confirm and cancel processing the contact records. Users can be directed to a particular page by returning a page reference (of the form "Page.*Visualforce page name*") in the action, or null to remain on the same page. If no records are selected on the first page, then instead of being directed to the confirmation page, the user will remain on the selection page, and an error message will be displayed.

In order for menus like this to work, the two pages must share the same controller type. Also, ensure that all page references used to change the page do not have the redirect attribute set to true, otherwise the users selections will be lost as the controller is reset.

Below is the Apex controller and Visualforce page code for the example.

Controller:
public class WorkingWithApexDataTablesController {

 public List<ContactWrapper> allContacts { get; set; }
 public Boolean allChecked { get; set; }
 
 public WorkingWithApexDataTablesController () {
  allContacts = new List<ContactWrapper>();
  allChecked = false;
  
  for(Contact contact: [select Name, Title, Department, Email                                   from Contact ]){ 
   allContacts.add(new ContactWrapper(contact));
  } 
 }
 
 public PageReference CheckAll(){
  
  for(ContactWrapper contact : allContacts){
   contact.selected = allChecked;
  }
  
  return null;
 }
 
 public PageReference ProcessSelectedContacts(){
  
  // If at least one contact has been selected,
  // forward to confrimation page
  for (ContactWrapper contactWrapper : allContacts ){
   if(contactWrapper.selected == true){
    return Page.WorkingWithApexDataTablesConfirmation;
   }
  }  
  
  // if no contacts have been selected, write an error message
  Apexpages.addMessage(new ApexPages.Message (ApexPages.Severity.ERROR,                                               'No contacts selected'));
  return null;
 }

 public PageReference confirmSelectedContacts(){
   
  List<String> selectedContacts = new List<String>{};   

  for (ContactWrapper contactWrapper : allContacts ){
   if(contactWrapper.selected == true){
    selectedContacts.add(contactWrapper.con.Name);
   }
  }
  
  // otherwise, write a confirmation message 
  Apexpages.addMessage(new ApexPages.Message (
     ApexPages.Severity.CONFIRM, selectedContacts.size() + 
     ' contacts selected ' + selectedContacts));
  
  // If all contacts have been selected, write a warning message
  if(selectedContacts.size() == allContacts.size()){
   Apexpages.addMessage(new ApexPages.Message (
     ApexPages.Severity.WARNING, 'All contacts selected'));
  }
   
  return Page.WorkingWithApexDataTables;
 }
 
 public PageReference cancelConfirmSelectedContacts(){
   
  // If the users cancels the action at the confirmation screen, 
  // return them to the main screen with a cancellation message
  Apexpages.addMessage(new ApexPages.Message (
    ApexPages.Severity.CONFIRM, 'Action Cancelled'));   
   
  return Page.WorkingWithApexDataTables;
 }
 
 public List<Contact> getSelectedContactObjects(){
  List<Contact> selectedContactObjects = new List<Contact>{};
  
  for (ContactWrapper contactWrapper : allContacts ){
   if(contactWrapper.selected == true){
    selectedContactObjects.add(contactWrapper.con);
   }
  }
  
  return selectedContactObjects;
 }

 public class ContactWrapper {
  
  public Contact con{get; set;}
  public Boolean selected {get; set;}
        
  public ContactWrapper(Contact c){
   con = c;
   selected = false;
  }
 }
}

WorkingWithApexDataTables Page:
<apex:page controller="WorkingWithApexDataTablesController">
  
  <apex:sectionHeader title="Working With Apex Data Tables"/>
  
  <apex:pageMessages />
  
  <apex:form >

    <apex:dataTable value="{!allContacts}" var="c" id="contactsTable">
      <apex:column >
        <apex:facet name="header">
          <apex:inputCheckbox value="{!allChecked}">
            <apex:actionSupport event="onclick" action="{!CheckAll}"
                                rerender="contactsTable"/>
          </apex:inputCheckbox>
        </apex:facet>
        <apex:inputCheckbox value="{!c.selected}"/>
      </apex:column>
      <apex:column value="{!c.con.Name}" headervalue="Full Name"/>
      <apex:column value="{!c.con.Title}" headervalue="Title"/>
      <apex:column value="{!c.con.Department}" 
                   headervalue="Department"/>
      <apex:column value="{!c.con.Email}" headervalue="Email"/>
    </apex:dataTable>

    <apex:commandButton action="{!ProcessSelectedContacts}" 
                        value="Process Selected Contacts"/>

  </apex:form>
</apex:page>

WorkingWithApexDataTablesConfirmation Page:
<apex:page controller="WorkingWithApexDataTablesController">
  
  <apex:sectionHeader                                                           title="Working With Apex Data Tables (Confirmation)"/>
  
  <apex:form >

    <apex:outputText value="You have selected the following contacts for                            processing, are you sure?"/>

    <apex:dataTable value="{!selectedContactObjects}" var="con"                             id="selectedContactsTable">
      <apex:column value="{!con.Name}" headervalue="Full Name"/>
      <apex:column value="{!con.Title}" headervalue="Title"/>
      <apex:column value="{!con.Department}" headervalue="Department"/>
      <apex:column value="{!con.Email}" headervalue="Email"/>
    </apex:dataTable>

    <apex:commandButton action="{!CancelConfirmSelectedContacts}" 
                        value="Cancel"/>

    <apex:commandButton action="{!ConfirmSelectedContacts}" 
                        value="Confirm"/>

  </apex:form>
</apex:page>

Thursday, 3 February 2011

Working with Apex data tables: Displaying selections and errors using page messages

In my opinion, Apex page messages is one of the most useful and straightforward Salesforce development features available. When developing custom functionality inside Visualforce pages, we can leverage page messages to give a user direct feedback. We could inform them for example that an action has been successful, or explain why an error has occurred.

The controller code to create a new page message and add it to the current page:
Apexpages.addMessage(new ApexPages.Message(ApexPages.severity,                                                  messageBody));
for example:

Apexpages.addMessage(new ApexPages.Message (ApexPages.Severity.ERROR,                                           'Hello World Error'));

To display the messages on the page is simply a case of using the apex:pageMessages tag, like so:
<apex:pageMessages/>

In the following example I have taken the majority of code from a previous blog post concerning creating a check-all checkbox on a data table. I have added apex messages in the previously empty perform action method to illustrate how they can be used. The following logic is applied in this method:

  • If no contacts are selected, an error message will be generated.
  • If all contacts are selected, a warning message will warn that the maximum amount of contacts have been selected.
  • In at least one contact has been selected, a confirmation message will be displayed, including a list of the selected contacts' names.
Here are some screenshots:
(Screen before interaction)


(No records Selected)


(4 contacts Selected)

Controller:
public class WorkingWithApexDataTablesController {

 public List<ContactWrapper> allContacts { get; set; }
 public Boolean allChecked { get; set; }
 
 public WorkingWithApexDataTablesController () {
  allContacts = new List<ContactWrapper>();
  allChecked = false;
  
  for(Contact contact: [select Name, Title, Department, Email                                   from Contact ]){ 
   allContacts.add(new ContactWrapper(contact));
  } 
 }
 
 public PageReference CheckAll(){
  
  for(ContactWrapper contact : allContacts){
   contact.selected = allChecked;
  }
  
  return null;
 }
 
 public PageReference ProcessSelectedContacts(){
  
  List<String> selectedContacts = new List<String>{};
  
  for (ContactWrapper contactWrapper : allContacts ){
   
   if(contactWrapper.selected == true){
     selectedContacts.add(contactWrapper.con.Name);
   }
  }
  
  if(selectedContacts.isEmpty()){
    // if no contacts have been selected, write an error message
    Apexpages.addMessage(new ApexPages.Message (                                ApexPages.Severity.ERROR, 'No contacts selected'));
  }
  else {
    // otherwise, write a confirmation message 
    Apexpages.addMessage(new ApexPages.Message (                                ApexPages.Severity.CONFIRM, selectedContacts.size() +                   ' contacts selected ' + selectedContacts));
  
    // If all contacts have been selected, write a warning message
    if(selectedContacts.size() == allContacts.size()){
        Apexpages.addMessage(new ApexPages.Message (                                    ApexPages.Severity.WARNING, 'All contacts selected'));
    }
  }
  
  
  return null;
 }

 public class ContactWrapper {
  
  public Contact con{get; set;}
        public Boolean selected {get; set;}
        
        public ContactWrapper(Contact c){
            con = c;
            selected = false;
        }
        
 }
}

Page:
<apex:page controller="WorkingWithApexDataTablesController">
  
  <apex:sectionHeader title="Working With Apex Data Tables"/>
  
  <apex:pageMessages />
  
  <apex:form >

    <apex:dataTable value="{!allContacts}" var="c" id="contactsTable">
      <apex:column >
        <apex:facet name="header">
          <apex:inputCheckbox value="{!allChecked}">
            <apex:actionSupport event="onclick" action="{!CheckAll}"
                                rerender="contactsTable"/>
          </apex:inputCheckbox>
        </apex:facet>
        <apex:inputCheckbox value="{!c.selected}"/>
      </apex:column>
      <apex:column value="{!c.con.Name}" headervalue="Full Name"/>
      <apex:column value="{!c.con.Title}" headervalue="Title"/>
      <apex:column value="{!c.con.Department}" 
                   headervalue="Department"/>
      <apex:column value="{!c.con.Email}" headervalue="Email"/>
    </apex:dataTable>

    <apex:commandButton action="{!ProcessSelectedContacts}" 
                        value="Process Selected Contacts"/>

  </apex:form>
</apex:page>

This is just a brief exploration of messages, but still shows how they can greatly improve the user experience. For more about messages, see the full apex page messages class definition, which includes the full enumeration of severity values. Also note that individual apex:pageMessage tags can be used to split up the messages to display at different points on the page and control their content directly on the page, should you so desire.