Oh my god. It's full of code!

Displaying and Caching Salesforce Attachment Images in Sites

This time around we are going to be talking about images. How to store them, how to query for them, display them and cache them, in Salesforce, using javascript remoting. We’ll be building a simple application using jQuery, Salesforce and Apex to query for attachments, display them and cache them reduce load times and overhead.

Abstract:
First off, I’m having a bit of a hard time organizing all my thoughts on this topic. It’s kind of big, so please forgive if I skip around a bit. Feel free to ask for clarifications in the comments. So let’s say you are building an application to be hosted on Salesforce. Your application is going to need to publicly accessible (so you are going to be using sites) and the application is going to need to show images that may change frequently and hence would be configured by some non developer types. Your application is going to show all the products you have available, along with pictures of said products.

There is of course many ways you can go about storing your images and relating them to your products but the most straight forward option is to use the notes and attachments feature. That would allow users to easily manage the pictures related to each opportunity without having to go to some central picture repository, or building any additional relationships between objects or URLS. The problem of course is that attachments don’t have a publicly accessible URL to them. You can view them from within Salesforce but you don’t have any way to display them on a site. This could be an issue. Not so fast!

Images as Data
You know those images you uploaded to Salesforce via the attachments feature exist somewhere on Salesforce servers. We also know that Salesforce hates file storage and loves databases. It should come as little surprise that the attachments are actually stored in a table as blob data. That data can be queried for just like any other data. Another little know thing is that in HTML while the img tag normally has it’s src attribute set to a URL, it can in-fact accept base64 encoded image data by specified the data type (). Perhaps we can put all this information together into something useful. Yes, yes we can.

Getting The Image Data
So go ahead and get a visualforce page and controller set up. I’m calling mine productList and productListController respectively. Let’s get the code for our controller in place. Copy and paste this.

global class productListController
{

    //get all the products in the org along with their attachments.
    @remoteAction
    global static remoteObject getProducts()
    {
        remoteObject returnObj = new remoteObject();

        try
        {

            list<Product2> products = [select 
                                                Name,
                                                ProductCode,
                                               Description,
                                               Family,
                                               isActive,
                                               (SELECT Attachment.Name, Attachment.Id FROM Product2.Attachments)
                                               from product2
                                               where isActive = true];
            returnObj.sObjects = products;
        }
        catch(Exception e)
        {
            returnObj.success = false;
            returnObj.message = 'Error getting products';
            returnObj.data = 'Error Type: ' + e.getTypeName() + ' ' + e.getCause() + ' ' + ' on line: ' +e.getLineNumber(); 
        }

        return returnObj;       
    }

    //gets a single attachment (photo) by id. The data is returned as a base64 string that can be plugged into an html img tag to display the image.
    @RemoteAction
    global static remoteObject getAttachment(id attachmentId)
    {   
        remoteObject returnObj = new remoteObject();
        try
        {
            list<Attachment> docs = [select id, body from Attachment where id = :attachmentId limit 1]; 
            if(!docs.isEmpty())
            {
                returnObj.data = EncodingUtil.base64Encode(docs[0].body); 
            }    
        }
        catch(exception e)
        {
            returnObj.success = false;
            returnObj.message = e.getMessage();
            returnObj.data = 'Error Type: ' + e.getTypeName() + ' ' + e.getCause() + ' ' + ' on line: ' +e.getLineNumber();        
        } 
        return returnObj;    
    }   

    global class remoteObject
    {
        public boolean success = true;
        public string message = 'operation successful';
        public string data = null;
        public list<sObject> sObjects = new list<sObject>();
    }    
}

As you can see it’s a pretty simple little controller. We have one method that gets a listing of all the products and the Id’s of the associated attachments using a subquery. That prevents us from having to run another query to get the attachment Id’s. The second function takes a specific attachment id and will return an object with the base64 encoded version of the image. That’s what I was talking about earlier. You can query for an attachment and get it’s raw binary/blob data. Then you can base64 encode it for transfer from the controller back to the requesting page. With that you can get the image data out Salesforce and to your public application.

This does introduce another problem though. Caching. Normally images would be cached by the browser when they are loaded. It uses the filename to create a cached version of the image so next time your browser needs to load it it can just pull it off the hard drive instead of across the internet. The problem with base64 images is they can’t really be cached easily. By the time you have enough data to find it in the cache, you already loaded the whole thing, totally defeating the entire point of the cache. How can we fix this? Caching is too important to just skip in most applications, but yet we need to use base64 encoded images in our app.

Local Storage
With HTML5 we now have something called local storage. Basically it lets us store just about anything we want on the users computer for use at a later time. Basically cookies on steroids. Also where as cookies had to be small little text files, local storage gives us much more flexibility with size. We can leverage this to build own our cache.

Here is the game plan. We’ll run our query to find all the products. We’ll loop over each product we find and create a create an img tag that contains the ID of the image/attachment that needs to go there. After that, we’ll loop over each image tag and populate it with the image. We’ll check to see if we have a local storage item with the ID of the image/attachment. If so, we’ll load that data from the local cache. If not, we’ll make a remoting call to our Apex getAttachment method, and cache the results with local storage then load the data into the img tag. Here is what that looks like.

<apex:page controller="productListController">
    <head>
    <title>Product List</title>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.js"></script>

    <script>
        $(document).ready(function() {
            getProducts(function(){
                $('.cacheable').each(function(index){
                    var img = $(this);
                     getImage($(this).attr('id'),function(imageId,imageData){
                         $(img).attr('src', 'data:image/png;base64,'+imageData);
                     });
                });               
            });            
        });  

        function getProducts(callback)
        {
                    Visualforce.remoting.Manager.invokeAction(
                        '{!$RemoteAction.productListController.getProducts}',
                        function(result, event)
                        {
                            if (event.status && (result.success == true || result.success == 'true')) 
                            {    
                               var html='';
                               for(var i = 0; i<result.sObjects.length;i++)
                               {
                                   var imageId = 'default image id here';
                                   if(result.sObjects[i].hasOwnProperty('Attachments'))
                                   {
                                        imageId = result.sObjects[i].Attachments[0].Id;
                                   }
                                   html += '<li><img class="cacheable"  id="'+imageId+'">'+result.sObjects[i].Name+'</li>';

                               }
                               $('#products').html(html);
                               callback();
                            } 
                            else
                            {
                                $("#responseErrors").html(event.message);
                            }
                        }, 
                        {escape: true});                   
        } 

        function getImage(imageId,callback)
        {
             var imageData;

              if ( localStorage.getItem(imageId))
              {   
                console.log('Getting image from local storage!');
                imageData = localStorage.getItem(imageId);
                callback(imageId,imageData);    
              }
              else 
              {
                   console.log('Getting image remote server!');
                    Visualforce.remoting.Manager.invokeAction(
                        '{!$RemoteAction.productListController.getAttachment}',
                        imageId,
                        function(result, event)
                        {
                            if (event.status && (result.success == true || result.success == 'true')) 
                            {    
                                 imageData = result.data;
                                 localStorage.setItem(imageId,imageData);      
                                 callback(imageId,imageData);    
                            } 
                            else
                            {
                                $("#responseErrors").html(event.message);
                            }
                        }, 
                        {escape: true});                   
              }      
        } 
    </script>
    </head>

    <body>
            <ul  id="products"></ul>
    </body>            
</apex:page>

So if you are familiar with jQuery and callbacks it’s pretty easy to make sense of what’s going on here. Once the DOM loads we are going to call the getProducts function. getProducts is going to use remoting to run the getProducts apex method. It will iterate over the results and create a list item for each product as well as that empty tag with the id attribute we talked about earlier. It also assigns the img tag the cacheable class so we can easily iterate over them once we are done. Once the looping and list building is complete, we call the callback functions. Since remoting requests are asyncronous we need to use callbacks when we only want to call one function when the other has completed first. Callbacks are a bit beyond the scope of this article, but just know that if we didn’t use them the get $(‘.cachable’).each() loop would run before the list had finished being populated.

So anyway getProducts finishes running and creating the list. Then comes the loop that uses jQuery to find any element that has the ‘cacheable’ class. For each element it finds, it calls the getImage() function on it, passing in the Id of that element. GetImage is where the cacheing magic happens. It will check to see if a local storage item exists with the id it gets passed. If so, it calls back with that content, if not, it queries Salesforce for an attachment with that id, creates a local storage element for it, and then again returns that content. The loop takes the returned content and sets the src tag of the img element with the base64 encoded data and boom! We have an image.

There you have it. Using Salesforce attachments to house images, using Apex and jQuery to query for them and display them, and HTML5 local storage to cache them. Pretty cool eh? I could write more, but I’m tired and I don’t feel like it. Hit me with questions if ya got em.

16 responses

  1. Reblogged this on Sutoprise Avenue, A SutoCom Source.

    August 16, 2012 at 3:25 pm

    • Sam

      Hi Kenji,
      Thanks for the awesome post. It is pretty much close to what i want. The one you posted is for displaying all product attachments limit 1. but i would like to display all attachments for a Product intead of limiting to 1. Can you please tell me the code tweaks i need to do. Please help me out.

      August 22, 2012 at 5:46 am

      • Hey Sam,
        All you need to do to get all the images would be to turn the hard reference to the 1st attachment into a loop that iterates over all of them in the javascript segment.

        Here is a link to a pastebin that shows you how to do it (I haven’t actually tried it, so there may be syntax errors, but it should get you pretty close).

        http://pastebin.com/jxeenaTh

        Hope this helps!

        August 22, 2012 at 3:38 pm

      • Sam

        Thanks Kenji for reply… You are so helpful….And also i would like to implement Onclick functionality for the displayed image attachments. Is there any way that we can do using VF Tags .

        August 22, 2012 at 3:41 pm

    • gaurav raj

      Hi,
      I need to write a trigger to get url of a pdf when the status for the pdf is set as completed in a custom field. How can i query to get the link of the pdf and make it a hyperlink

      October 1, 2013 at 2:49 pm

  2. Sam

    I am trying to display attachments by trying apex:pageblocktable…………apex:image and apex:repeat…but i didnot get anything positive..please throw me some light..

    August 22, 2012 at 3:45 pm

    • I’m not sure about using VF tags, since that all gets rendered serverside and this javascript stuff happens client side, so it’s too late for VF to do anything. You pretty much have to accept when using javascript remoting to provide data that you can’t use any visualforce components with that data. You are going to have to build your interface using good old fashioned html, css, javascript and DOM manipulation. As for your onclick handler, what I would do is in the jQuery loop that iterates over all the cachable elements include a jquery bind event. Something like

      $(img).bind(‘click’,function(){alert(‘hello!’)});

      right after the line that says

      var img = $(this)

      That will tell jQuery to attach a function (which in this case will show the alert box) whenever a user clicks one of the images.

      August 22, 2012 at 3:49 pm

      • Sam

        Thanks Kenji…you are awesome i will try that and let u know the result.. thanks much:)

        August 22, 2012 at 3:53 pm

  3. Hi kenji,
    I want to add an Integer to the controller that specifies the number of products to display, however, when i add public Integer myInteger; i can’t use it from the remote function, i get the error variable does not exist, is there a way to use variables declared in the controller level from within the remote function, thanks.

    October 15, 2012 at 1:12 pm

    • You’d have to make the integer have a getter/setter and set it to public on the controller. Something like

      integer myInt{get;set;}

      public integer getmyInt()
      {
      return 1;
      }

      Then in your visualforce page you can reference it with

      {!myInt}

      Which you can also use in your javascript. Hope this helps.

      October 15, 2012 at 2:30 pm

  4. A person essentially help to make significantly posts I’d state. This is the very first time I frequented your web page and up to now? I amazed with the research you made to create this particular post amazing. Wonderful process!

    January 5, 2013 at 5:53 am

  5. Gonzalo

    First of all I want to say terrific blog! I had a quick question in
    which I’d like to ask if you don’t mind. I was curious to know how
    you center yourself and clear your head prior to writing.
    I have had a tough time clearing my thoughts in getting my ideas out.

    I truly do enjoy writing however it just seems like the first 10 to 15 minutes tend to be lost
    just trying to figure out how to begin. Any suggestions or
    tips? Cheers!

    January 5, 2013 at 9:29 am

  6. Hey! I know this is somewhat off topic but I was wondering which blog platform
    are you using for this site? I’m getting fed up of WordPress because I’ve had problems with hackers and I’m looking at alternatives for another platform. I would be great if you could point me in the direction of a good platform.

    January 6, 2013 at 8:26 am

  7. My brother suggested I may like this web site.
    He was entirely right. This publish actually made my
    day. You cann’t imagine simply how so much time I had spent for this information! Thanks!

    January 7, 2013 at 9:43 am

  8. You actually make it seem so easy with your presentation but I
    find this matter to be actually something which I think I would never
    understand. It seems too complicated and extremely broad for me.
    I am looking forward for your next post, I’ll try to get the hang of it!

    August 14, 2013 at 9:19 pm

  9. Just desire to say your article is as astonishing.
    The clarity in your post is just spectacular and i can assume
    you are an expert on this subject. Fine with your permission let me to grab your feed to keep updated with forthcoming post.

    Thanks a million and please carry on the rewarding work.

    July 4, 2014 at 6:38 am

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s