# Saturday, 07 November 2009
Update 2/22/2010 See this article for a better way of using Ajax with Visualforce/Apex.

JQuery and JSON have become my tools of choice for designing and developing Single Page Applications (or SPA). Why?
  1. The user experience is better if the entire page is not refreshed when executing a single action
  2. There's a vast library of JQuery plug-ins and a Developer community to tap into.
  3. Using JSON as the standard object model and separating the UI from the database allows me to design a UI only once and port the application to other cloud platforms (for example, the VF code below can run on i-Dialogue, Google Apps Engine, or Azure by only changing a few lines of code).

Credit goes to Ron Hess for showing me this pattern.I've made very little changes to his Apex code example, except to add JQuery and handling actionFunction rerender callbacks slightly differently.

The basic pattern I'm using for developing Single Page Applications for Visualforce is to call actionFunctions from JQuery, which in turn call Apex controller methods that construct JSON strings. The web page then handles the JSON formatted response in re-rendered outputPanels. The code below demonstrates a simple auto complete function that searches Salesforce Accounts that match a user entered input on each keypress. Clicking on an Account drills down to Account details by calling another actionFunction that retrieves specific Account information.

Some Caveats

  • The outputPanel scripts will execute the first time the page is loaded, so a check for null is required in the callback to prevent first-time execution
  • The dynamic re-rendering of script within an outputPanel makes it difficult to do true functional programming and create closures that elegantly handle the callbacks. More complex applications may have to utilize global state variables, increasing the mutability of the application (and potential for bugs)
  • The mutability of Apex controller properties requires a one-to-one relationship between actionFunction handlers and their corresponding response strings.
  • actionFunctions send the page ViewState in the AJAX XmlHttpRequest and return a blob of XML (apparently using the Sarissa open source library) in the response, which has a slightly slower performance than what you'd get using JQuery's native AJAX utility methods.

Source Code

  1. <apex:page sidebar="false" showHeader="false" standardStylesheets="false" title="AJAX Development Harness" controller="exampleCon">
  2. <style>
  3. body {font-family: Verdana;}
  4. .apexTable { width: 600px;}
  5. .evenTableRow {background-color: #eee;}
  6. .defaultHidden {display: none;}
  7. style>
  8. <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js">script>
  9.     <h3>Using JQuery and JSON with Visualforceh3>
  10.     <apex:form >
  11.         <apex:actionFunction name="apex_searchAccounts" action="{!SearchAccounts}" immediate="true" rerender="searchResultsPanel">
  12.             <apex:param name="searchTerm" value="" />
  13.         apex:actionFunction>
  14.         <apex:actionFunction name="apex_getAccountDetails" action="{!GetAccountDetails}" immediate="true" rerender="accountDetailsPanel">
  15.             <apex:param name="accountid" value="" />
  16.         apex:actionFunction>
  17.     apex:form>
  19.     Enter Account Name: <input type=text id='accountInput' /> (type the letters 'uni' to test)<br/>
  20.     <div id="accountsListView" class="view">div>
  21.     <div id="accountDetailsView" class="view defaultHidden">div>
  23.     <script type="text/javascript">
  24.         $(function() {
  25.             $("#accountInput")
  26.                 .focus()
  27.                 .keyup( function (e) {
  28.                     var searchTerm = $("#accountInput").val();
  29.                     if( searchTerm != ''){
  30.                         apex_searchAccounts(searchTerm);
  31.                     }
  32.             });
  34.             $(".accountDetailsLink").live('click', function() {
  35.                 apex_getAccountDetails( $(this).attr('id') );
  36.             });
  37.         });
  39.         function clearAllViews(){
  40.             $(".view").html('');           
  41.         }
  43.         function renderAccountsListView(accounts){         
  44.             if(accounts.length === 0){
  45.                 $("#accountsListView").html("no accounts matching that query").fadeIn();
  46.                 return;
  47.             }
  48.             var tableHTML = '';
  49.             for(var i=0; i < accounts.length; i++){
  50.                 tableHTML += '
  51. ';
  52.             }
  53.             tableHTML += '
  54. + accounts[i].id + '" href="#" class="accountDetailsLink">' + accounts[i].name + '
  55.             $("#accountsListView").html(tableHTML).fadeIn();
  56.             $("table tr:nth-child(even)").addClass("evenTableRow");
  57.         }
  59.         function renderAccountDetailView(account){         
  60.             var tableHTML = '

    Account Details for ' + account.name + '

  61.             tableHTML += '';         
  62.             tableHTML += '
  63. ';
  64.             tableHTML += '
  65. ';
  66.             tableHTML += '
  67. ';
  68.             tableHTML += '
  69. ';           
  70.             tableHTML += '
  71. Name' + account.name + '
    Type' + account.type + '
    Description' + account.description + '
    Website+ account.website + '" target=_blank>' + account.website + '
  72.             $("#accountDetailsView").hide().html(tableHTML).slideDown();
  73.             $("table tr:nth-child(even)").addClass("evenTableRow");
  74.         }
  75.     script>
  76.     <apex:outputPanel id="searchResultsPanel" layout="block" rendered="true">
  77.         <script>
  78.         function apex_searchAccounts_callback(jsonResponse){           
  79.             if(jsonResponse === null || jsonResponse === ''){
  80.                 return;
  81.             }
  83.             var accounts;
  84.             eval('accounts = ' + jsonResponse);
  85.             if(accounts !== null){
  86.                 renderAccountsListView(accounts);
  87.             }
  88.         }
  89.         clearAllViews();
  90.         apex_searchAccounts_callback('{!JSONSearchAccounts}');
  91.         script>
  92.     apex:outputPanel>    
  94.     <apex:outputPanel id="accountDetailsPanel" layout="block" rendered="true">
  95.         <script>       
  96.         function apex_getAccountDetails_callback(jsonResponse){        
  97.             if(jsonResponse === null || jsonResponse === ''){
  98.                 return;
  99.             }
  101.             var account;
  102.             eval('account = ' + jsonResponse);
  103.             if(account !== null){
  104.                 renderAccountDetailView(account);
  105.             }
  106.         }
  107.         apex_getAccountDetails_callback('{!JSONAccountDetails}');
  108.         script>
  109.     apex:outputPanel>    
  110. apex:page>
  112. //Controller
  114. public class exampleCon {                  
  115.     public String JSONSearchAccounts{ get; set; }
  117.     public PageReference SearchAccounts() {
  118.         String searchTerm = Apexpages.currentPage().getParameters().get('searchTerm');         
  119.         String soql = 'SELECT Id, Name FROM Account WHERE Name like \'' + searchTerm + '%\'';              
  120.         List<Account> accountList = Database.query(soql);          
  122.         string json = '[';
  123.         for(Account acct : accountList){
  124.             json += '{"id": "' + acct.Id + '", ' +
  125.             '"name": "' + acct.Name + '"},';
  126.         }
  127.         json += ']';
  128.         json = json.replace('},]', '}]');
  130.         JSONSearchAccounts = json.replace('\'', '');
  131.         return null;
  132.     }
  134.     public String JSONAccountDetails{ get; set;}
  136.     public PageReference GetAccountDetails() {
  137.         String accountid = Apexpages.currentPage().getParameters().get('accountid');           
  138.         String soql = 'SELECT Id, Name, Type, AccountNumber, Description, Website FROM Account WHERE Id=\'' + accountid + '\'';            
  139.         List<Account> accountList = Database.query(soql);
  141.         if(accountList.size() != 1){
  142.             JSONAccountDetails = '{"error": "Expected only 1 account"}';
  143.             return null;
  144.         }          
  146.         string json = '{';     
  147.         json += '"id": "' + accountList[0].Id + '", ';     
  148.         json += '"name": "' + accountList[0].Name + '", ';
  149.         json += '"type": "' + accountList[0].Type + '", ';
  150.         json += '"accountNumber": "' + accountList[0].AccountNumber + '", ';
  151.         json += '"description": "' + accountList[0].Description + '", ';
  152.         json += '"website": "' + accountList[0].Website + '" ';
  153.         json += '}';       
  155.         JSONAccountDetails = json.replace('\'', '');
  156.         return null;
  157.     }
  158. }
Saturday, 07 November 2009 13:55:33 (Pacific Standard Time, UTC-08:00)
# Friday, 06 November 2009

So, an email arrives in my inbox today inviting me to try Windows Azure. I've tried Azure a couple times in the past with not much luck. I got the impression from reading some white papers that there was bottom-up innovation going on at the hardware and storage layer and that they had solved the scalability challenges first, but there wasn't much to show from the top-down (unfortunately, where first impressions are made).

But it is Microsoft, so if they're really serious about cloud computing, you got to at least sit up and listen. Here's my 5 minute unedited foray back into the world of Azure.

(Yes, I know the transparency of my video reveals management keys, yadda, yadda. It's a sandbox. I trust you...)

Friday, 06 November 2009 18:09:02 (Pacific Standard Time, UTC-08:00)

Because the format of Salesforce record identifiers are deterministic, there's always a concern when using them in Site URLs that someone will 'guess' record IDs and gain access to records.

If the data you're publishing is public anyway, like Solutions, Events, or Press Releases, then maybe it's no big deal. But when Sites are being used as landing pages for sensitive data, like Opportunities or Partner Accounts, then some more effort is required to protect the records.

Since most records correlate with an Account or Contact, in a portal context it's simple enough to check the record relationship. Otherwise, URL encryption is really the only option for anonymous visitors.

A couple blog entries here and here discuss the use of MD5 hash for creating encrypted URLs.

These solutions use custom fields and triggers to correlate a web page request with an originating request record.

With the recent release of custom settings in Winter 10, there's now the option of storing private keys and using PKI encryption to sign record identifiers and decrypt them.

Unfortunately, the Crypto class documentation is a bit 'cryptic' (pun intended) on how to implement full PKI encryption, since it only demonstrates how to sign strings, and not decrypt them. Hopefully, someone can enlighten me on how this might work. I have a C# RC4 encryption engine that I'm considering converting to Apex, but I obviously prefer to use the native Crypto class, if possible.

(Sample code from Apex Developers Guide)
  1. String algorithmName = 'RSA';
  2. String key = 'pkcs8 format private key';
  3. Blob privateKey = EncodingUtil.base64Decode(key);
  4. Blob input = Blob.valueOf('12345qwerty');
  5. Crypto.sign(algorithmName, input, privateKey);
Friday, 06 November 2009 00:34:25 (Pacific Standard Time, UTC-08:00)
# Wednesday, 04 November 2009

I'm hooked on Single Page Applications (or SPA for short). If you work in Google Apps everyday, then you know what I mean. Open up GMail, Calendar, or even a Google map, and there's just a single page load. All subsequent interactions occur asynchronously via AJAX, producing a fluid, responsive user interface.

Contrast this with a traditional web application where you fetch a page and are given a master list of records. Click on a record and a completely new page loads containing record details (aka Master-Detail pattern).

Visualforce and Dialogue Script are inherited from a paradigm that mostly encouraged multi-page apps. I've spent the greater part of 2009 re-writing just about every single DScript app to be a SPA, and now that I'm developing applications for Force.com I'm going through the same exercise.

First, don't make the same mistake I did and assume that an AJAX application using Visualforce is as simple as using the AJAX Toolkit. It'll work as a VF page within Salesforce, but can't be published to Sites for security reasons. I found this fact most unfortunate since the Salesforce AJAX API is one of the best AJAX frameworks I've worked with as it encourages functional programming (FP) and embraces all the good parts about Javascript (yes, I am trying to shed my 15 years of Object-Oriented programming and learn FP... a blog entry for another day).

A SPA treatment can be given to a Visualforce page through the use of the <apex:actionFunction /> and <apex:outputPanel /> controls, which I'll cover in detail in Part 2.

The swim lanes diagram below (click here for the full version) demonstrates the lifecyle of a Visualforce SPA page.

1) The browser requests the web page which is rendered with default data.
2) Subsequent page clicks call Apex controller methods via actionFunction.
3) The controller methods construct JSON formatted strings. 3) When the request is complete, actionFunctions re-render outputPanels that contain script templates for processing the returned JSON arrays.

Wednesday, 04 November 2009 21:42:09 (Pacific Standard Time, UTC-08:00)

If you need a primer on "what is the cloud" from a consumer perspective, my other blog has a good post that explains my definition.

When I talk about "development in the cloud", it basically means implies three things:

  • The application or service is available on the Internet
  • The code you develop is managed and hosted on an Internet server
  • The application is aggregating web services found on the Internet

I won't get into the specifics of multi-tenancy, or any other operational aspects of cloud computing, since there are many means to the same end of delivering services in the cloud.

Cloud services aren't always HTML web applications, although that's primarily what I focus on. Rich Internet Applications (RIA) built on frameworks such as Flash, Flex, and Silverlight are also strong components of the future of cloud computing.

Wednesday, 04 November 2009 15:37:51 (Pacific Standard Time, UTC-08:00)

Part of the reasoning behind the name "Embracing" the cloud is to capture the transformation taking place in my own product design and development methodology.

We've listed various apps on the Salesforce.com AppExchange since 2006, but none were what are called "native" apps running on the Force.com servers. One goal for 2010 is to develop and list up to 3 native apps on the AppExchange using Salesforce Sites.

My first application built using Sites can be found here. I'll write up a more detailed blog post in the next few days describing the process behind this site.

Wednesday, 04 November 2009 14:58:30 (Pacific Standard Time, UTC-08:00)
Running a small software company requires a healthy focus on communicating the value of your products and services without getting too technical. I founded Cubic Compass in 2001, where we continue to focus on creating great Service and Support portal solutions that are easy for Line of Business managers to deploy and use.

But Programmer-specific content is often too overwhelming for line of business managers, so at "Embracing the cloud", I'm carving out a little corner of the blogosphere where I can write about my passion of writing code for cloud services.

Some things I'll write about, in no particular order:
  • XOS and Dialogue Script (a server-side web framework I developed and maintain)
  • JQuery (my client-side language of choice)
  • Javascript (Assembly/C language for the Internet. Very powerful. I'm always looking to push the JS envelope)
  • Force.com Visual Force and Apex (Salesforce.com now offers much more than CRM)
  • Google Code API
  • Google Android (mobile devices are a key component of the cloud. I don't do iPhone development. Support the Droid!)
  • Softlayer (Where I host stuff)
  • Amazon EC2 (another place I host stuff)
  • Authorize.net (My preferred gateway for credit card processing)
  • Anything hosted in the cloud with a public API
If this sounds interesting to you, please take a moment to add me to your RSS reader. Also, if you write on a similar topic, please send me a link to your blog and I'll add it to my blogroll.

Happy coding in the cloud!
Wednesday, 04 November 2009 14:29:29 (Pacific Standard Time, UTC-08:00)