Monday, 14 May 2012

Cloudstock London 2012


In an unexpected turn of events, I'm going to be presenting a community session at Cloudstock London 2012.  Unexpected as I hadn't planned on this, but a slot came open at the last minute and I agreed to fill in.

The upside to this is that I didn't have much time to think about what I was committing to.  The downside was that I had to decide on my topic and produce a description in a few hours.  What to speak about then?   It had to be something I was familiar with that wouldn't take a huge amount of time to prepare, as Cloudstock was just over a week away.  Looking at the list of existing community sessions, there was a theme of deep dives into specific technical areas - @wesnolte on Javascript Remoting and MVC Frameworks for example, or @pbatisson on Advanced Force.com Testing Techniques.

It struck me that there was an opportunity here to reach those attendees with little or no experience of Force.com, or those with some experience looking to progress.  There was already a breakout session introducing the Force.com platform, so there was little point in a session with the same technical angle.  Therefore I decided to look at this from a different perspective, and cover starting out as a Force.com developer, what employers are looking for and career progression.

This seemed like a good starting point, but unlikely to fill a 30 minute session unless I spoke very slowly! As followers of this blog know, if there's one thing I'm pretty familiar with that's Salesforce.com Certification. Most Salesforce/Force.com careers will involve gaining certifications - its certainly a key metric for Cloud Alliance Partners - so I'll be talking about the benefits, what the exams involve and how to prepare.  Since I gained the Technical Architect accreditation I've had a lot of interest in the exam, particularly the Review Board, so I'll be covering that too and expecting questions!

So if you are thinking about a job in Force.com development, or looking to gain recognition and advance your career through Certification, join me at Cloudstock from 12:30 to 1pm to find out more.

Wednesday, 2 May 2012

Opportunity Status Chart

An important aspect when working on Opportunities (or anything with a process that a record progresses through) is to knowing how far through the Sales Process a particular Opportunity is.  While some users may be familiar enough with the Sales Process to be able to figure this out immediately from the Stage Name, a visual indicator always helps.

Something I've been working on for a while now is using a line chart for just this purpose.  The key elements of this are:


  • List all of the Stages for the Opportunity - if record types are in use, this should be the Stages applicable to that record type
  • Plot a green line and markers for Stages that have been completed (or skipped, if the Opportunity has jumped into the process at an advanced staged
  • Plot a red line and markers for Stages still to come

The first point had always been a sticking point, but I managed to solve this using back in January by allowing Visualforce to render the specific picklist for the record, and then sending this back to the controller prior to processing the record, as detailed in this post.

It will come as no surprise to regular readers of this blog that I chose to handle the second and third points using the Dojo Charting framework.  I've finally completed the charting code  - below is a screenshot showing the chart embedded into the standard Opportunity record view page:



The controller simply walks the Stage Names and adds them to a complete list until it encounters the current state.   All Stage Names after this go into a todo list - note that both Closed-Won and Closed-Lost appear, and also the default '-- None-- entry - probably not what is desired and left as an exercise for the avid student.  The page is as follows:

<apex:page standardController="Opportunity" extensions="OpportunityStatusChartController" showheader="false" standardstylesheets="false" sidebar="false">
<head>
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js"
        djConfig="parseOnLoad: true,
      modulePaths: { 
          'dojo': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo', 
          'dijit': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit', 
         'dojox': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojox' 
             }
    ">
  </script>
  <link rel="stylesheet"
    href="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css" />
</head>
<body class="claro">
  <apex:form id="frm">
    <apex:actionFunction name="reloadWithStages" action="{!reload}" />
    <div id="test1" style="width: 100%; height: 150px;"></div>
    <apex:outputPanel layout="block" id="vals" style="display:none">
      <apex:inputField value="{!Opportunity.StageName}" required="false" id="stages"/>
      <apex:inputText value="{!valsText}" required="false" id="back"/>
    </apex:outputPanel>
  </apex:form>
  <apex:outputField value="{!Opportunity.StageName}" rendered="false"/>

  <script>
  function reload()
  {
     var ele=document.getElementById('{!$Component.frm.stages}');
     var idx=0;
     var valText='';
     for (idx=0; idx<ele.length; idx++)
     {
        valText+=ele.options[idx].text + ':';
     }
   
     var backele=document.getElementById('{!$Component.frm.back}');
     backele.value=valText;
   
     reloadWithStages();
  }

  <apex:outputPanel layout="none" rendered="{!NOT(loadOnce)}">
    dojo.require("dojox.charting.Chart2D");
    dojo.require("dojox.charting.axis2d.Default");  
    dojo.require("dojox.charting.plot2d.Default");
    dojo.require("dojox.charting.plot2d.StackedLines");
    dojo.require("dojox.charting.plot2d.Columns");
    dojo.require("dojox.charting.plot2d.Bars");
    dojo.require("dojox.charting.plot2d.ClusteredBars");
    dojo.require("dojox.charting.plot2d.StackedBars");
    dojo.require("dojox.charting.plot2d.Bubble");
    dojo.require("dojox.charting.plot2d.Grid");
    dojo.require("dojox.charting.plot2d.Pie");

    dojo.require("dojox.charting.themes.PlotKit.green");

    dojo.require("dojox.charting.action2d.Highlight");
    dojo.require("dojox.charting.action2d.Magnify");
    dojo.require("dojox.charting.action2d.MoveSlice");
    dojo.require("dojox.charting.action2d.Shake");
    dojo.require("dojox.charting.action2d.Tooltip");

    dojo.require("dojox.charting.widget.Legend");

    dojo.require("dojo.colors");
    dojo.require("dojo.fx.easing");
    dojo.require("dojo.date.stamp");
    dojo.require("dojo.date.locale");

    makeCharts = function(){

 var myMap={
  <apex:repeat value="{!labels}" var="label">
      '{!label.idx}':'{!label.text}'<apex:outputText value="," rendered="{!label.idx!=labelCount}"/>
  </apex:repeat>
            };
            
 var chart1 = new dojox.charting.Chart2D("test1");
 chart1.setTheme(dojox.charting.themes.PlotKit.green);
 chart1.addPlot("default", {type: "Default", lines: true, markers: true, tension:2});
 chart1.addAxis("x",
 { 
    majorTick: {stroke: "black", length: 3},
    majorTickStep:1, 
  minorTicks: false, 
  microTicks: false,
  min: 0,
  max: {!labelCount},
  rotation:30, 
  font: "6pt Tahoma",
  labels: [
  <apex:repeat value="{!labels}" var="label">
      {value: {!label.idx}, text:'{!label.text}'}<apex:outputText value="," rendered="{!label.idx!=labelCount}"/>
  </apex:repeat>
  ]
  
 });
 
 chart1.addSeries("cleared", 
   [
       <apex:repeat value="{!doneStageNumbers}" var="stage">
    {x: {!stage}, y: 2},
    </apex:repeat>
   ]);
 chart1.addSeries("todo", 
   [
       <apex:repeat value="{!todoStageNumbers}" var="stage">
    {x: {!stage}, y: 2},
    </apex:repeat>
   ],
   {plot: "default", stroke: {color:"#FE2E2E"}}
   );
   
 var myMap={
  <apex:repeat value="{!tooltips}" var="tooltip">
   "{!tooltip.idx}": "{!tooltip.text}",
  </apex:repeat>
            };
            
 var anim1a = new dojox.charting.action2d.Magnify(chart1, "default");
 var anim1b = new dojox.charting.action2d.Tooltip(chart1, "default",
 {
     text : function(o) {
         return (myMap[o.x])
                       }
        });
   
 chart1.render();
    };
  </apex:outputPanel>
   dojo.addOnLoad(
     <apex:outputPanel layout="none" rendered="{!loadOnce}">
        reload
     </apex:outputPanel>
     <apex:outputPanel layout="none" rendered="{!NOT(loadOnce)}">
 makeCharts
     </apex:outputPanel>
      );
  </script>
</body>
</apex:page>

Essentially this page has two distinct functions - the first to render a hidden form containing an input field for the StageName field, and then submit the values back via javascript, while the second is to actually render the chart based on the values.  The completed stages are plotted as a separate series to those that remain, to allow that part of the chart to be rendered in a different colour and markers. I've also thrown in a tooltip on each of my markers so that I can display some help/explanation text about each of the stages.

The controller is quite a simple one:
public class OpportunityStatusChartController
{
 public List<Tuple> labels {get; set;}
 public List<Tuple> tooltips {get; set;}
 public List<Integer> doneStageNumbers {get; set;}
 public List<Integer> todoStageNumbers {get; set;}
 public Integer labelCount {get; set;}
 public Boolean loadOnce {get; set;}
 public String valsText {get; set;}
        private Opportunity opp;
 
 public OpportunityStatusChartController(ApexPages.StandardController std)
 {
  opp=(Opportunity) std.getRecord();
  loadOnce=true;
 }
 
 public PageReference reload()
 {
  init();
  loadOnce=false;
  
  return null;
 }
 
 
 public void init()
 {
  labels=new List<Tuple>();
  tooltips=new List<Tuple>();
  doneStageNumbers=new List<Integer>();
  todoStageNumbers=new List<Integer>();
  
  labelCount=0;
  Boolean done=false;
  labels.add(new Tuple(labelCount++, '.'));
  
  for (String val : valsText.split(':'))
  {
   if (!done)
   {
    doneStageNumbers.add(labelCount);
   }
   else
   {
    todoStageNumbers.add(labelCount);
   }
    
   if (val==opp.StageName)
   {
    done=true;
    todoStageNumbers.add(labelCount);
   }
   labels.add(new Tuple(labelCount, val));
   toolTips.add(new Tuple(labelCount, 'Help for ' + val + ' stage'));
   labelCount++;
  }  
  labels.add(new Tuple(labelCount, '.'));
 }
 
 public class Tuple 
 {
  public Integer idx {get; set;}
  public String text {get; set;}
  
  public Tuple(Integer inIdx, String inText)
  {
   idx=inIdx;
   text=inText;
  }
 }
}

The labels are returned in a containing Tuple class along with an index, which is the x-axis position of the ploy.  Everything is plotted at the same level on the y-axis, as I want a straight line chart.

I can then change the record type of the Opportunity, save it and the chart updates accordingly:


You can see a live example of this on my demo site - simply click on one of the Opportunities to see the progress chart.


Sunday, 22 April 2012

Mobile Apps with Visualforce and JQuery Mobile

I've been spending some of my time building mobile functionality recently.  One thing that has put me off in the past has been the multitude of devices that need to be supported if I want to maximize usage.  Building the same functionality for iOS, Android and others doesn't fill me with joy.

HTML5 has the "write once, run anywhere" capability that first attracted me to Java back in JDK 1.0 days. The downside to this it that it does reduce the functionality available.  For example, offline storage is problematic at the moment with different browsers supporting different standards, and the standards themselves being subject to change, while access to native functionality is still in very early days (Android makes the camera available through javascript but there aren't many other examples out there).   It very much feels like the future though, so I've been heading down that route.

My first foray into developing a mobile front end was for our BGFM product for Dreamforce 2010.  This was simply some Visualforce pages sized and styled appropriately for the iPhone.  The obvious downside to that was that a device with any other form factor needed separate pages (or at least pages that could adjust themselves appropriately).  However, towards the middle of 2011 I came across JQuery Mobile (JQM) and starting using the 1.0 alpha releases.  The great thing about JQM is that it takes care of the cross device side of things, leaving you with one app that works across all popular smartphones, tablets etc.  Even better, it will also work against older devices, dropping back down to basic HTML if necessary.  For details, documentation, tutorials and samples, check out the JQuery Mobile site.

Building Visualforce pages that leverage JQM is pretty straightforward.  Lifting from the getting started page from the JQM site:

<apex:page showHeader="false" sidebar="false" standardStyleSheets="false">
<html> 
    <head> 
    <title>My Page</title> 
    <meta name="viewport" content="width=device-width, initial-scale=1" /> 
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.css" />
    <script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.js"></script>
</head> 
<body> 

<div data-role="page">

    <div data-role="header">
        <h1>My Title</h1>
    </div><!-- /header -->

    <div data-role="content">   
           <ul data-role="listview" data-inset="true" data-filter="true">
          <li><a href="#">Acura</a></li>
          <li><a href="#">Audi</a></li>
          <li><a href="#">BMW</a></li>
          <li><a href="#">Cadillac</a></li>
          <li><a href="#">Ferrari</a></li>
           </ul>
    </div><!-- /content -->

</div><!-- /page -->

</body>
</html>
</apex:page>

Opening this in a mobile device gives:




















As you can see from the code, I've had to do nothing to style this appropriately for the device, simply giving the unordered list a data-role of listview means that JQM takes care of all the heavy lifting. Adding buttons for simple navigation is also straightforward, and JQM provides some transitions to mimic native apps. Again, its all taken care of by the markup:

<a href="index.html" data-role="button" data-inline="true">Cancel</a>
<a href="index.html" data-role="button" data-inline="true" data-theme="b">Save</a>

resulting in a couple of appropriately styled buttons at the bottom of the page:





















Where things get a little more interesting is if you have a form in the page and the buttons are submitting the form rather than simply moving to a new page, as is the case in most of the Visualforce pages I write.

In that case I'd like to use an <apex:commandLink> component, but I can't provide the additional attributes to lay things out correctly - e.g. data-inline="true". While I could write some Javascript to take care of this, in the first instance I'm trying to keep things simple, so I use an <apex:actionFunction> component and tie this to the JQM specific link via an onlick handler as follows:

   <apex:form id="jsfrm">
    <apex:actionFunction action="{!save}" name="save"/>
          .....
      <a href="#" data-role="button" data-inline="true" 
                onclick="$.mobile.showPageLoadingMsg(); save()">Save</a>
          .....
   </apex:form>

The $.mobile.showPageLoadingMsg(); function call simply displays a spinner to let the user know something is happening.

 Standard <apex:inputField/> components also render well most of the time - sometimes I end up using the input text/textarea/checkbox etc to allow for a greater level of control, but a basic form can be created with the minimum of effort.  For example, a simple form to create an account by specifying its name and industry only requires the following markup:

<apex:page showHeader="false" sidebar="false" standardStyleSheets="false" standardController="Account">
<html> 
    <head> 
    <title>Create Account</title> 
    <meta name="viewport" content="width=device-width, initial-scale=1" /> 
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.css" />
    <script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.js"></script>
</head> 
<body> 

<div data-role="page">

    <div data-role="header">
        <h1>Create Account</h1>
    </div><!-- /header -->

    <div data-role="content">   
      <apex:form id="jsfrm">
        <apex:actionFunction action="{!save}" name="save"/>
        <apex:outputLabel for="name" value="Name"/>
        <apex:inputField id="name" value="{!Account.Name}"/>
        
        <apex:outputLabel for="industry" value="Industry"/>
        <apex:inputField id="industry" value="{!Account.Industry}"/>
        
        <a href="index.html" data-role="button" data-inline="true">Cancel</a>
        <a href="index.html" data-role="button" data-inline="true" data-theme="b" 
                   onclick="$.mobile.showPageLoadingMsg(); save();">Save</a><br />
      </apex:form>
     </div><!-- /content -->

</div><!-- /page -->

</body>
</html>
</apex:page>

Produces the page shown below - not too bad for a few lines of markup:




















Obviously once the record is saved, the user is redirected to the standard view page which looks pretty awful on a phone, so standard controllers probably aren't going to be a silver bullet for this, making it the usual challenge for editions below enterprise.

I've put together a demo application that allows a contact to take a survey. Its hosted on an unauthenticated Force.com site so is publicly available.  A couple of sample screen shots are shown below:



If you'd like to try out the demo application, simply click here.

A couple of points to note:

  • I've put this together over a few weekends so I wouldn't be in the least bit surprised if there were some glitches waiting.  
  • I've only tested it with an iPhone and webkit browser, though I can't see why there would be issues with different devices.
  • This is a real functioning application, so the feedback will be stored in my Salesforce instance - so if you feel like leaving real feedback, I may even act on it!

Friday, 6 April 2012

Re-order Formula Report Columns

A non-Visualforce/Apex blog for a change this week. I was working on some reports for our Salesforce project management application, and needed to create a number of summary formula columns. Having created the first three, I wanted to change the order that these formula columns appeared in the report, and was surprised to find that this isn't supported through the UI - if you remove all formula columns from the report and add them back in, they are displayed in the order that they were created.   Searching on the Salesforce Idea Exchange only threw up this open idea.

Below is an example report I've put together to demonstrate this using Accounts.  I've added a single formula column that totals the account employees based on the country:
















I then add a column to total the Annual Revenue, which displays to the right of the Country Employees column:














If I then decide that I'd like the Country Revenue column displayed first, the only way to achieve this through the UI is to delete the formula columns and recreate them in the desired order.  Maybe acceptable when there are only a couple of formula columns, but not an option once there's more than a handful.

Next up was to turn to the metadata API via the Force.com IDE.  As I'd just gone with the defaults when setting up the project, the only metadata components I had were Classes, Pages, Components and Triggers.  Right-clicking (or ctrl-click as I am now a Macbook Air user) the project name brings up the context menu for the project - selecting the Force.com menu followed by the Add/Remove Metadata Components brings up the following dialog:



















Clicking the Add/Remove button (eventually) brings up a list of the available metadata.  Reports appears so I tick the entry for my report:
























and click ok as many times as it takes to close the various dialogs.  When prompted, I select Yes to refresh the project.  Once the revised components have been pulled down from the server, opening the file associated with my report shows :

    <aggregates>
        <calculatedFormula>EMPLOYEES:SUM</calculatedFormula>
        <datatype>number</datatype>
        <developerName>FORMULA1</developerName>
        <isActive>true</isActive>
        <masterLabel>Country Employees</masterLabel>
        <scale>2</scale>
    </aggregates>
    <aggregates>
        <calculatedFormula>SALES:SUM</calculatedFormula>
        <datatype>number</datatype>
        <developerName>FORMULA2</developerName>
        <downGroupingContext>ACCOUNT.ADDRESS1_COUNTRY</downGroupingContext>
        <isActive>true</isActive>
        <masterLabel>Country Revenue</masterLabel>
        <scale>2</scale>
    </aggregates>

Looking promising - not only can I see the formula columns but they also have developer names that imply some sort of ordering. First up I tried changing the order that the columns appear in the XML and saving that. Unfortunately, this change was simply reverted when the file was changed.

Next, I changed the developerName for each column - changing the Country Employees to FORMULA2 and Country Revenue to FORMULA1. Looking better once the save completed - the ordering of the columns in the file had been reversed as shown below:

    <aggregates>
        <calculatedFormula>SALES:SUM</calculatedFormula>
        <datatype>number</datatype>
        <developerName>FORMULA1</developerName>
        <downGroupingContext>ACCOUNT.ADDRESS1_COUNTRY</downGroupingContext>
        <isActive>true</isActive>
        <masterLabel>Country Revenue</masterLabel>
        <scale>2</scale>
    </aggregates>
    <aggregates>
        <calculatedFormula>EMPLOYEES:SUM</calculatedFormula>
        <datatype>number</datatype>
        <developerName>FORMULA2</developerName>
        <isActive>true</isActive>
        <masterLabel>Country Employees</masterLabel>
        <scale>2</scale>
    </aggregates>

Encouraging, but the acid test is to re-run the report:






















Success!  Comparing with the report screenshot above, the Country Revenue and Country Employees columns have changed places.  Not quite a simple as it might be through the UI, but certainly a lot quicker than deleting and recreating formula fields!



Saturday, 24 March 2012

Create Parent and Child Records in One Insert Call

If you're anything like me, creating parent and child records in Apex is something that has to be done on a pretty regular basis. This blog post is one I've had on the list for a while, but it came up on the discussion boards yesterday (March 2012) so it seemed like as good a time as any to write it up properly.

The simplest way to achieve this is to insert the parent, then set the lookup relationship id on the child to the parent record id, as follows:

Account acc=new Account(Name='Blog Acc1');
insert acc;

Contact cont=new Contact(FirstName='Bob', LastName='Buzzard', AccountId=acc.id);
insert cont;

the downside to this approach is that it is unlikely to scale.  If I need to insert a large number of accounts and contacts, I'll have to manage the relationships myself in some way - probably using wrapper classes to  combine an account and its list of contacts, with multiple iterations to insert the accounts, then populate the lookup fields.  All in all quite a lot of code.

The next avenue I explored was setting the child relationship field to the parent record:

Account acc=new Account(Name='Blog Acc2');
insert acc;

Contact cont=new Contact(FirstName='Bob', LastName='Buzzard', Account=acc);
insert cont;

No dice on this I'm afraid - even though I've inserted the parent account first, the child contact is stored without a parent account.

One of the techniques that I came across while studying for the Technical Architect Certification was to specify the parent object via an external id.  So I created an external id field on my account named Master_Id__c and inserted an account:

Account acc=new Account(Name='Blog Acc3', Master_Id__c='Blog Acc3');
insert acc;

Once the account is in place, I can instantiate the parent record based on the external id and set the relationship field:

Account acc=new Account(Master_Id__c='Blog Acc3');
Contact cont=new Contact(FirstName='Bob', LastName='Buzzard', Account=acc);
insert cont;

Looking better, as I don't have to set ids, but I'm still inserting the parent first and then the child. Perhaps its possible to instantiate a new account and contact and then insert them later:

Account acc=new Account(Name='Blog Acc 4', Master_Id__c='Blog Acc 4');
Contact cont=new Contact(FirstName='Bob', LastName='Buzzard', Account=acc);
insert acc;
insert cont;

Once again, no dice. The same result as the second attempt - the account and contact are inserted, but the relationship is lost. Given that there's very little difference between this and the last attempt, it looks like its the name that is causing the problem. After trying a few permutations, the following code confirmed that this is the case:

Account acc=new Account(Name='Blog Acc 6', Master_Id__c='Blog Acc 6');
insert acc;

Contact cont=new Contact(FirstName='Bob', LastName='Buzzard', Account=new Account(Name='Blog Acc 6', Master_Id__c='Blog Acc 6'));
insert cont;

This throws the following exception - System.DmlException: Insert failed. First exception on row 0; first error: INVALID_FIELD, More than 1 field provided in an external foreign key reference in entity: Account: [].
 I've no idea why this exception isn't thrown when I specify a previously instantiated account rather than instantiating as part of the contact record, but there it is.   Given this error message, it seemed possible to create a contact and identify the account by Name, but that didn't work either - a similar exception complaining that Name isn't an external id or indexed field.

 This did guide me to the preferred solution though - simply instantiating a new account as part of the contact and only specifying the external id:

Account acc=new Account(Name='Blog Acc 7', Master_Id__c='Blog Acc 7');
insert acc;

Contact cont=new Contact(FirstName='Bob', LastName='Buzzard', Account=new Account(Master_Id__c='Blog Acc 7'));
insert cont;

Looking a lot better, but there are still two insert statements - luckily insert statements can take a list of generic sobjects to insert, so I can insert both objects in one go. As long as the parent record is inserted first, everything works as expected:
Account acc=new Account(Name='Blog Acc 8', Master_Id__c='Blog Acc 8');
Contact cont=new Contact(FirstName='Bob', LastName='Buzzard', Account=new Account(Master_Id__c='Blog Acc 8'));

insert new List<Sobject>{acc, cont};

Monday, 19 March 2012

Salesforce Certified Advanced Administrator



This week saw the final piece in the Salesforce Certification puzzle, as I gained the Advanced Administrator Certification and now have the full set of 8.  Unfortunately, one of these (the original Consultant Certification) retires at the end of March, so I only have a couple of weeks before the total drops to 7.

As usual, please don’t ask for or post any actual exam questions.  Anything of this nature will be removed immediately.

The exam is 60 question with a time limit of 90 minutes and a pass mark of 67%.  In a refreshing change from the last few exams I’ve taken, the questions tend to be short and snappy and quite a lot of them fit onto a single line.  It was also unusual, in that I can’t remember any other exams where I had to know the difference between the editions, remember the permissions required for actions and identify the menu sections for various configuration items.  Make sure that you read the questions properly - its easy to overlook a ‘not’ or a ‘false’ if you skim read - if you find that there are two valid answers, but the question only requires one, check that you haven’t got the wrong end of the stick.

The study guide is the first port of call when preparing for this exam - everything that came up was mentioned.  Areas that you need know are:




  • Privacy (sharing) and security - these come up in every Salesforce Certification exam - if you don’t fully understand this side of things they are all going to be challenging.
  • Community setup - Ideas and Answers, along with their data categories.
  • Territory management - how it differs from the standard role hierarchy, pre-requisites for enabling
  • The various desktop tools - Connect for Office, Outlook Connector, Salesforce for Outlook, Offline Edition, Lotus Notes Connector, Apex Data Loader.
  • CRM Content - luckily I’ve trained a few customers on this so I had plenty of resources to fall back on.
  • Formula fields - best practice for efficient formulas, the various formula functions
  • Buttons and links, overriding standard and creating custom
  • Extending the platform using the web services API, Apex and Visualforce.  You don’t need to know the syntax or reference details, more the concepts of when its appropriate to use them and what the pros and cons are.
  • Capabilities conferred upon a delegated administrator.
  • Forensic investigations - I’d not come across this prior to reading the study guide - another good reason to read it!
  • How to author and deploy apex code and applications
  • Sorage usage - what does and doesn’t count against storage limits, plus strategies to manage storage effectively.
  • Workflow and approvals, how these can be used to automate business processes and where these fit into the order of execution along with triggers, validation rules, assignment rules etc
  • What you can and can't do with the page layout editor

I think this exam would be a struggle if you tried to memorise the configuration information as opposed to working with the system for some time.  Salesforce adds more and more functionality with each release, so there it would be a huge amount to remember.  Plus as an Advanced Administrator you will be expected to know the limits of the ‘clicks not code’ approach and how you’d go about customizing and extending via the APIs.  You will also need to know how to investigate problems and suspicious activity.



And finally a word for the blogger editor - this really excelled itself today. The contents of this post were deleted three times, HTML markup randomly embedded into text and continual failures to save.  This made what should have been a simple job extremely painful.

Saturday, 3 March 2012

Integrate Custom Date Picker with Visualforce

If you've used Visualforce input fields backed by a Date or DateTime sobject field, one thing you'll have noticed is the date range isn't that helpful.  For example, the screenshot below is from my developer edition and the starting year is 2011.

 
 
 
This is fine for opportunities, cases, campaigns etc., that have dates in the future, but less than useful when recording a date in the past - for example year of birth.  There's a couple of options here.  The first is to use some javascript on the page to tweak the setup of the date picker.  Not a solution I'd recommend, as a change to the underlying implementation would break the page.
 
The other option is to use a custom date picker.  I favour the Design2Develop implementation, as a lot of the others have style class names that overlap with the Salesforce implementation, which could cause problems if I wanted to combine both on the same page.
 
The first thing to do is get the zip file from the download page and then upload it as a static resource to your Salesforce organization.  I chose the name JSCalendar - if you choose a different one, change the $Resource entries in the sample code.
 
Next, create the Visualforce page and add following lines to include the core Javascript and styling:
 
(UPDATE 09/01/2014 - the latest zip needs the calendar folder specified as the location for the JavaScript and CSS - if you are having problems make sure these your imports match these lines)
<apex:includeScript value="{!URLFOR($Resource.JSCalendar,’calendar/calendar.js')}"/>
<apex:stylesheet value="{!URLFOR($Resource.JSCalendar,’calendar/calendar_blue.css')}" />

I've gone for the blue style, but there are several others in the zip file.   Next, define the date format that you'll be using - this is achieved by implementing the fnSetDateFormat function:

function fnSetDateFormat(oDateFormat)
{
 oDateFormat['FullYear'];  //Example = 2007
 oDateFormat['Year'];   //Example = 07
 oDateFormat['FullMonthName']; //Example = January
 oDateFormat['MonthName'];  //Example = Jan
 oDateFormat['Month'];   //Example = 01
 oDateFormat['Date'];   //Example = 01
 oDateFormat['FullDay'];   //Example = Sunday
 oDateFormat['Day'];    //Example = Sun
 oDateFormat['Hours'];   //Example = 01
 oDateFormat['Minutes'];   //Example = 01
 oDateFormat['Seconds'];   //Example = 01
 
 var sDateString;
 
 // Use dd/mm/yyyy format
 sDateString = oDateFormat['Date'] +"/"+ oDateFormat['Month'] +"/"+ oDateFormat['FullYear'];
 return sDateString;
}

Next, define the function that will manage the integration between the date picker and the input element. This takes two parameters - the object that fired the calendar popup (e.g. an image or button) and the id of the input element that contains the date.  This function will be called when the picker is displayed, and is responsible for extracting the current value from the input element and passing that to the library function.

function initialiseCalendar(obj, eleId)
{
 var element=document.getElementById(eleId);
 var params='close=true';
 if (null!=element)
 {
  if (element.value.length>0)
  {
   // date is formatted dd/mm/yyyy - pull out the month and year
   var month=element.value.substr(3,2);
   var year=element.value.substr(6,4);
   params+=',month='+month;
   params+=',year='+year;
  }
 }
 fnInitCalendar(obj, eleId, params);
}

Finally, add an input component.  Its possible to change the standard behaviour of the apex:inputField using javascript to tie to to the custom picker as opposed to the standard, but again this is a fragile solution and would preclude using standard and custom on the same, so I prefer use an apex:inputText instead.  As can be seen, the object that is passed to the initialiseCalendar function is the input field that is also identified by the id parameters - I could change the function to take a single parameter, but if I then wanted to have an image that is clicked on to open the calendar I'd be out of luck. I've made this the onmouseover event handler so that it opens in similar way to the standard picker.

<apex:inputText id="startdate" size="10" value="{!Campaign.StartDate}" onmouseover="initialiseCalendar(this, '{!$Component.startdate}')"/>

Here's a screen shot of the new picker:
There's a fair bit more you can do to customize this picker - check out the docs on the download page.

For the sake of completeness here is the full markup of the Visualforce page:

<apex:page standardController="Campaign">
<apex:includeScript value="{!URLFOR($Resource.JSCalendar,’calendar/calendar.js')}"/>
<apex:stylesheet value="{!URLFOR($Resource.JSCalendar,’calendar/calendar_blue.css')}" />
 <apex:pageMessages />
 <apex:form id="frm">
   <apex:pageblock id="pb">
     <apex:pageBlockButtons >
        <apex:commandButton value="Save" action="{!save}" />
        <apex:commandButton value="Cancel" action="{!Cancel}" />
     </apex:pageBlockButtons>
     <apex:pageBlockSection id="pbs">
     <apex:inputField value="{!Campaign.Name}" />
  <apex:inputText id="startdate" size="10" value="{!Campaign.StartDate}" onmouseover="initialiseCalendar(this, '{!$Component.startdate}')"/>
  <apex:inputText id="enddate" size="10" value="{!Campaign.EndDate}" onmouseover="initialiseCalendar(this, '{!$Component.enddate}')"/>
 </apex:pageBlockSection>
   </apex:pageblock>
 </apex:form>
 <script>
function fnSetDateFormat(oDateFormat)
{
 oDateFormat['FullYear'];  //Example = 2007
 oDateFormat['Year'];   //Example = 07
 oDateFormat['FullMonthName']; //Example = January
 oDateFormat['MonthName'];  //Example = Jan
 oDateFormat['Month'];   //Example = 01
 oDateFormat['Date'];   //Example = 01
 oDateFormat['FullDay'];   //Example = Sunday
 oDateFormat['Day'];    //Example = Sun
 oDateFormat['Hours'];   //Example = 01
 oDateFormat['Minutes'];   //Example = 01
 oDateFormat['Seconds'];   //Example = 01
 
 var sDateString;
 
 // Use dd/mm/yyyy format
 sDateString = oDateFormat['Date'] +"/"+ oDateFormat['Month'] +"/"+ oDateFormat['FullYear'];
 return sDateString;
}
  
    
function initialiseCalendar(obj, eleId)
{
 var element=document.getElementById(eleId);
 var params='close=true';
 if (null!=element)
 {
  if (element.value.length>0)
  {
   // date is formatted dd/mm/yyyy - pull out the month and year
   var month=element.value.substr(3,2);
   var year=element.value.substr(6,4);
   params+=',month='+month;
   params+=',year='+year;
  }
 }
 fnInitCalendar(obj, eleId, params);
}
 </script>
</apex:page>