Tuesday, June 4, 2013

Showing images in your custom Visualforce pages via Attachments

Everyone wants images, profile pictures, logos etc. Sure you can add these via style, but this isn't very practical if the end user is adding them (they dont know, or need to know, how to use static resources). So lets say you have a task to make a page that shows lists of contacts, but they want to see the contacts image, if they have one. If they dont, we will have to show a default user image in its place to keep each contact consistant. 

This is done very simply :-) On each contact record (or any object that you allow attachments on) the user has the ability to attach files and images. So we want to grab them via a relationship query on that object, and then display them on the page. For my example I am only going to show the most recent image per Contact.

First step is to create the page and controller. I called my class (controller) sfg_ContactPhotos so the page so far would look like this:


<apex:page showHeader="true" sidebar="true" controller="sfg_ContactPhotos">

</apex:page>

In the controller we will need a List of Contacts and a few methods to get it all going. Normally this would be a controller extension to the Contact standard controller, but since I just want to show the logic, I dont want to complicate things here. 



public class sfg_ContactPhotos {

  public List<Contact> lstOfContacts {get; set;}


  public sfg_ContactPhotos() {

    //constructor
  }

  private List<Contact> findContacts() {

    //logic for getting contacts
    return new List<Contact>();
  }

}



You may notice that I like to use the {get; set;} instead of the way salesforce normally does it via a get method and set method; this is simply because if it is done the salesforce way, the get method will be hit on every call to the controller and I only need to load the Contacts once. Anyhow, we need to add another variable for the Map of Attachments, a method to build the map, and it wouldn't hurt to wire up the variables to their methods in the constructor. If you notice the Map is a <String, String> which is not the *correct way, it should be Map<Id, Attachment> but due to issue with visualforce and Maps, it has to be String (see Post).


public class sfg_ContactPhotos {

  public List<Contact> lstOfContacts {get; set;}

  public Map<String, String> mapOfPhotos {get; set;}

  public sfg_ContactPhotos() {

    lstOfContacts = findContacts();
    mapOfPhotos = buildPhotoMap(lstOfContacts);
  }

  private List<Contact> findContacts() {

    //logic for getting contacts
    return new List<Contact>();
  }

  private Map<String, String> buildPhotoMap(List<Contact> pContacts) {

    if (pContacts  == null || pContacts.isEmpty()) {
      return new Map<String, String>();
    }

    Map<String, String> returnMap = new Map<String, String>();

    //logic to make the map of attachments
    return returnMap;
  }

}



A few things to note here, if you notice I am passing the list of Contacts into the buildPhotoMap method. I call it 'pContacts' due to it being a 'param' thus the 'p'. Since it is passed, this means that we can control what builds the map, instead of just grabbing the Contacts in their current state. I also check to see if what is passed actually has anything before creating any variables in the class, this is a good practice to get into. Why waste time creating variables if there isn't anything for the method to use? It may only be milliseconds but it can add up, so its good to do the check for null/empty/blank first and if nothing is there, exit the method, not wasting any time. 


Ok, lets add some logic to the class, first we will query for the Contacts and the related Attachments, then pass it to the buildPhotoMap method to get the data set up like we want. In this query we only want the public ones and will order the Attachments by LastModifiedDate 'desc' which pust the newest one first. We will limit the query to 2 just so we dont over load the page/example.


private List<Contact> findContacts() {
    return [select Name,
                   Email,
                   (select ContentType
                    from Attachments
                    where isPrivate = false
                    order by LastModifiedDate desc)
            from Contact
            limit 2];
  }


If you notice I am not querying for any Id's this is because by default SOQL returns the Id of the object so there is no need to specify it in the query. There is also no need for a try catch statement since if there is nothing to return it will simply return null, and in the buildPhotoMap method we are if it is null or empty before using it, thus negating any issues.

Now lets put some logic into the buildPhotoMap method, we will need to iterate over the contact list with a for loop and since the Attachments are in a related query we will need a second for loop over it to get into each attachment on the individual contact record. Once in the Attachments for loop we will need to check what type it is since you can have many different types of attachments, we should check if the ContentType is of type 'image'. (you could of corse add a where clause to the query itself and only return Attachments where ContentType = 'image/%' thus negating the check in the for loop)



private Map<String, String> buildPhotoMap(List<Contact> pContacts) {
    if (pContacts.isEmpty()) {
      return new Map<String, String>();
    }

    Map<String, String> returnMap = new Map<String, String>();
    for (Contact c : pContacts) {
      for (Attachment a : c.Attachments) {
        if (returnMap.containsKey(c.Id)) {
          break;
        }
        if (a.ContentType.contains('image/')) {
          returnMap.put(c.Id, a.Id);
          break;
        }
      }
      if (!returnMap.containsKey(c.Id)) {
        returnMap.put(c.Id, 'none');
      }
    }
    return returnMap;
  }


Since we need a map entry for each contact (so we dont brake the page with an error of 'Map key {ID} not found in map') and we dont want to loop more than we need to, I set it up so it brakes once it detects that the map already contains the contact Id. If it doesn't then it will check if the Attachment is of type 'image/' (normally the types are 'image/jpg' or 'image/gif' etc so using 'image/' will cover all images) and if it is, add it to the map and then brake out of the Attachment for loop and on to the next contact. If however, the Contact doest have any image Attachments, we will add a null value so the map wont brake the page.

Now that we have the controller done, lets focus on the page. We will need an apex:repeat over the contact list and an apex:image to display the default image (incase the user has no images)


<apex:page showHeader="true" sidebar="true" controller="sfg_ContactPhotos">
    <apex:stylesheet value="{!URLFOR($Resource. sfg_customPhotos, '/css/main.css')}" />
 
    <div id="sfg_pageWrap">
        <apex:repeat value="{!lstOfContacts}" var="c">

            <div id="{!c.Id}">
                <h1>{!c.Name}</h1>
                <apex:image url="{!URLFOR($Resource.sfg_customPhotos, '/image/userIcon.png')}" width="50" height="50"/>
            </div>

        </apex:repeat>
    </div>
</apex:page>


As you can see I have some style included via the apex:stylesheet tag, and a default image. We still need to add some conditional rendering and the Attachment Image to the page but first lets take a moment to talk about Resources.

Static Resources are very hand if you know how to use them. First off they are located in setup > develop > Static Resources. Click the new button and you'll see a page like this:

 The name will be the one used in the value="{!URLFOR($Resource.yourResourceNameHere,  ... ", the resource should be public, and the file should be a zip of the folder (if you want to have everything in one spot, otherwise you can just upload an image or a file, but if you want folders they have to be compressed or 'zipped'). Since I am including everything in one resource I have to zip my folders, which look like this on my computer:

So the page should now look like this once we fill it all out:
Click save and then if you need to update it you can click edit and upload the new .zip.

Since I am using folders, I have to travers them like would in any other language to access the items like '/css/main.css' as you can see in the page. Its the same for the image as well, '/image/userIcon.png'. Even if you dont have folders but a zip of lose files, you will need to '/someFileName.css' the '/' is important, without it some browsers (like IE) wont render it correctly.

Ok back to business, lets add the Attachment image to the page and then put in some conditional rendering based off weither the map is empty for that contact, and if so lets show a default image of a user:

<apex:image rendered="{!mapOfPhotos[c.Id] == 'none'}" url="{!URLFOR($Resource.sfg_customPhotos, '/image/userIcon.png')}" width="50" height="50"/>

<apex:image rendered="{!mapOfPhotos[c.Id] != 'none'}" url="{!URLFOR($Action.Attachment.Download, mapOfPhotos[c.Id])}" width="50" height="50" />

SO when we load the page we should see something like this:
...and yes .gif's do work :-)