Monday, May 5, 2014

Uploading Files – Send as an Email attachment of attach to a object

Introduction 

Uploading a document such as pdf, word, power point etc. or image is a very common requirement that you would come across when doing custom development.
A very good use case could be to provide users with the possibility to add documents to an email when they are sending emails from a visual force page. Your Visualforce page could look like this:



So how can this be achieved. With Visualforce it is much simpler than you would imagine.

This post covers a lot of conscepts such as;

  •          View state
  •          Transiemt variables
  •          inputFile component
  •          actionRegion componen
  •          Open activities vs Activity History
  •         Email attachments and sending emails etc
Read on and you may come across that „aha“ moment even if you are already a pros.

Visualforce

Visualforce offers a component called inputFile which you could use to achieved this;

 <apex:pageBlockSection title="Additional Attachments" columns="1" collapsible="false">  
      <apex:outputLabel value="Upload additional Attachments" styleClass="fieldLabel" />  
      <apex:outputPanel >  
      Please note the following limitations when uploading files:  
      <ul>  
           <li>The maximum email message size for a complete email message including attachments is <b>10MB</b></li>  
           <li>Attachments can be up to <b>5MB</b> per attachment</li>  
      </ul>  
      </apex:outputPanel>  
      <apex:outputPanel id="additionalAttachments">  
           <apex:repeat value="{!additionalAttachments}" var="attachment">  
                <apex:outputLabel value="File: " for="file" />  
                <apex:inputFile value="{!attachment.body}" filename="{!attachment.name}" id="file" />  
                <br />  
           </apex:repeat>  
      </apex:outputPanel>  
 </apex:pageBlockSection>  


Looking at the code above we can see that the apex:inputFile component requires a value which is he body oft he attachment. Notice the apex:repeat which we use to iterate through the variable or Apex Property !additionalAttachments. This acts like a kind of placeholder for the Attachments that will be uploaded by the user. We can use the size of this variable to control how many attachments we want user to be able to add.

Inputfile Gotcha

If you use Inputfile component and Attempt to use Visualforce Ajax rerender attribute or the oncomplete attribute you will run into this nasty stuff here.

apex:inputFile can not be used in conjunction with an action component, apex:commandButton or apex:commandLink that specifies a rerender or oncomplete attribute
As the error says, rerender and oncomplete are just not allowed together with inputFile component. Most of the time, you will have to do a full page load each time which can cause performance issues because you lose your amazing Ajax capabilities. But as i said, - most oft he time. Read on.

Tip – Workaround

In Visualforce there is a component call actionRegion. When you wrap a block within the actionRegion Tag, what you are doing is telling the Controller to consider only the piece of code wrapped by the actionRegion and ignore everything else. The name says it all, actionRegion(region or action). If you have a region you wish to rerender, that does not have anything to do with the inputfile enclosing it in an actionRegion will make sure that the Controller does not even know that an inputFile is on your page. This is an example on how to use this;

 </apex:actionRegion>  
      <apex:outputPanel id="addRecipientButton">  
           <apex:commandButton value="Add Recipient" action="{!addRecipient}" rerender="additionalRecipients, addRecipientButton" disabled="{!IF((additionalRecipients.size + OpportunityTeam.size) > 20 , 'true', 'false')}" />    
      </apex:outputPanel>  
 </apex:actionRegion>  


This technique will however not work, if you rerender the portion with the inputFile maybe to dynamically add more upload files like I did in the post, with dynamically adding fields for more email recipients. Checkout it out here.

Caution:

Make sure you understand what you are doing and what the implications are. There is a good reason why Ajax Techniques are not allowed together with the inputFile. Read here for more.

Upload Limit

Note that each attachments cannot be bigger than 5MB. Else you would get this beautiful message.


Apex

Now let’s look at the Controller/Apex code to give a full picture of how the file uploading works.

 /* List of additionalAttachments */  
 public List<Attachment> additionalAttachments { get; set; }  
 /* Number of default attachments */  
 public static final Integer NUMBER_OF_ATTACHMENTS_TO_ADD = 3;  
 /**  
  *Build attachment list with empty attachments to act as placeholder for attachments that the User may add 
  *  
  * @param  
  * @return  
  */  
 public void buildAttachmentList(){  
      this.additionalAttachments = new List<Attachment>();  
      for(Integer i= 0; i<NUMBER_OF_ATTACHMENTS_TO_ADD ; i++){  
           this.additionalAttachments.add(new Attachment());  
      }  
 }  


We declare a property additionalAttachments and a constant NUMBER_OF_ATTACHMENTS_TO_ADD and use this to create a certian number of empty attachments (placeholders) which are used on the Visualforce page. The method buildAttachmentList is called in the constructor. There are a couple of things you could to with the attachments uploaded by a user; attch them to an email, or attach them to an object. Fort he sake of completeness, I will briefly introduce these two possibilities here.

Send Uploaded files as attachments

 
/* Holds a list of attachments with a body. These are the attchments that have been selected by the user and the Apex code
 * will need to handle. This is transient because it will be used only by Apex code and does not need to be 
 * part of the View State.
 */  
 public transient List<Attachment> attachmentsToUpload { get; set; }  
 /* Holds a list of Email attchments. This is transient because it will be used only by Apex code and does not need to be 
  * part of the View State. Also defined as part of the instance so it is accessible by different methods
  */  
 public transient List<Messaging.Emailfileattachment> emailAttachmentsToSend { get; set; }  
 /**  
  * retreive attachments that were uploaded by the user. Only attachments with a body will be considered since
  * since the user may or may not have uploaded any attachments
  *  
  * @param  
  * @return Emailfileattchment  
  */  
 public void retrieveAdditionalAttachment(){  
      if(this.attachmentsToUpload == null){  
           this.attachmentsToUpload = new List<Attachment>();  
      }  
      if(this.emailAttachmentsToSend == null){  
           this.emailAttachmentsToSend = new List<Messaging.Emailfileattachment>();  
      }  
      for(Attachment att : this.additionalAttachments){  
           if(att.Body != null){  
                this.attachmentNames.add(att.Name);  
                attachmentsToUpload.add(att);  
           }  
      }  
      for(Attachment mesAtt : this.attachmentsToUpload){  
           Messaging.Emailfileattachment efa = new Messaging.Emailfileattachment();  
           efa.setBody(mesAtt.Body);  
           efa.setFileName(mesAtt.Name);  
           efa.setContentType(mesAtt.ContentType);  
           this.emailAttachmentsToSend.add(efa);  
      }  
      this.additionalAttachments = null;  
      this.buildAttachmentList();  
 }  


We define two transient variables to help us retreive the uploaded files and create email attachments out of them. We use the word transient, to make sure we do not litter our View State with variables that will not be used on the Visualforce page. Read the section below on View State to see why this is important. The user may not upload any files at all or he may upload less files than he is allowed to. For this reason we use attachmentsToUpload to hold files whose body is not null, meaning users uploaded a file. For each of the uploaded files, we construct an email attachment setting the email name, content type and body, and add this to emailAttachmentsToSend which will be referenced somewhere else when we want to send the email like this:
 Messaging.Singleemailmessage mail = new Messaging.Singleemailmessage();  
 mail.setFileAttachments(this.emailAttachmentsToSend);  


View State

View State is one of the most important concepts you have to understand when dealing with Visualforce. You can read about here. But to make a long story short: Html was not designed to maintain state during calls between the server and the browser. But part of the strength of Visualforce lies in its data binding abilities which uses Properties among other techniques to ensure that server and client side data is synchronized. To be able to do this Visualforce pages have to maintain state. This is done by by storing all data bindings in the View State. In Visualforce the View State is limited to 135KB. That is why at the end of the retrieveAdditionalAttachment method, we reinitilize the additionalAttachments by calling buildAttachmentList.

Another Important Limit

Note that the size of the email you can send including all emails cannot exceed 10MB. Else you get this nasty little fellow;



Did You Know?

Secondly when the complete size of your attachments exceed 3MB, the email sent out will not contain the files but links to these files.



Clicking on any of the links will download the file tot he user’s computer. These files are stored on Salesforce.com Servers for a period of 30 days after which they are deleted.

Attaching the Uploaded files to a task

A typical use case could be to make sure that, once an email goes out it is added to the Activity History. Now how you you do this?. Simply; by creating a task with the status set to completed.

 /**  
  * Documents the sending of the Report by creating an Entry in the Activity History. This is done by creating a task 
  * with the Status as "Completed" 
  *  
  * @param  
  * @return Id of the created task  
  */  
 public Id createCompletedTask(){  
      Id taskId;  
      Task completedTask = new Task();  
      completedTask.Subject = 'Opportunity Report';  
      completedTask.Status = 'Completed';  
      completedTask.Priority = 'Normal';  
      completedTask.Activity_Type__c = 'Other';  
      completedTask.OwnerId = Userinfo.getUserId();  
      completedTask.WhatId = this.opportunity.Id;  
      completedTask.ActivityDate = Date.today();  
      completedTask.Description = 'Email Recipients: ' + this.opportunity.Recipients_E_Mail__c + ' <br /> Email Content: ' + this.opportunity.Content_of_E_Mail__c;  
      try{  
           Database.Saveresult result = Database.insert(completedTask);  
           if(result.isSuccess()){  
                taskId = result.getId();  
           }       
           return taskId;  
      } catch(Exception e){  
           Apexpages.Message pmessage = new Apexpages.Message(Apexpages.Severity.ERROR, 'A task was not inserted for this Opportunity Report. Please contact your admin.');  
           Apexpages.addMessage(pMessage);  
           return null;  
      }  
 }  


This is pretty straight forward. All you need to do is check what fields a task can have and set the required ones and the ones that are relevant to you. By setting the Status to Completed we ensure that, the task is no longer an Open Activity but an Activity History entry. You could then be required to store the attachments as well together with this task, so that it is always clear which attachments were sent out with which email. How would you do this. Simple; add these attachments to the completed task like this:

 /**  
  * Adds the uploaded attachments to the Activity History Task 
  *  
  * @param taskId: Id of the task which to which the attachments are added. This will be the parent of the attachments  
  * @return   
  */  
 public void createAttachmentsForTask(Id taskId){  
      if(this.attachmentsToUpload != null){  
           if(!this.attachmentsToUpload.isEmpty()){  
                for(Attachment att : this.attachmentsToUpload){       
                     att.ParentId = taskId;  
                }  
           }  
           try{  
                insert this.attachmentsToUpload;  
           } catch (Exception e){  
                Apexpages.Message pmessage = new Apexpages.Message(Apexpages.Severity.ERROR, 'The following error occured while adding attachments to the created task: ' + e);  
                Apexpages.addMessage(pMessage);  
           }  
      }  
 }  


Here again we use our dear transient variable attachmentsToUpload to which holds the uploaded files and set thier parentId to the Id oft he task that was created after sending the email. Pretty straight forward. We then insert our attachments. If you are a beginner or a Pro, make it a habit to always anticipate sections of your code, that could cause errors and wrap these in try catch blocks. In this way you can handle exceptions and give your users meaningful error messages.

Conclusion

This post has touched quite a few things, some of them a must-know if you want to get good at Visualforce and Apex e.g. Viewstate, some rather tips e.g. use of actionRegion etc. I encourage you to challenge some of these ideas and suggest alternatives if you can. This post are meant to help other users learning the same stuff I have learnt. I love programming because it is so diverse. So share your ideas. Thanks for reading.

12 comments:

  1. A nice article addressing key steps and potential use cases for file upload with SFDC.
    It will be even better, if you can save the uploaded file to Box.Net / Amazon S3 / Rackspace / Google Cloud. That way, you can avoid the 10 MB size limitation and save the storage cost .

    ReplyDelete
    Replies
    1. Hi thanks for the comment, especially the idea with alternatives for handling uploaded files. I will take a look at them and add one to the post above for completion sake. Great idea

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Hi Viban,

    Great post! But, I still can't get that thing to work! I tried almost every available alternative in the universe but still NO LUCK!
    Also, Please tell me why Ajax is not allowed to work in conjunction with apex:inputFile?

    ReplyDelete
    Replies
    1. Hi Joshi, sorry i didn't reply early. Please let me know if you still need help with this. I have implemented this particular technique a couple of times. I can help you out if you tell me exactly what you are doing. If the ActionRegion Visualforce component doesn't help as mentioned above, I can also show you how to use an IFrame to achieve file uploads.
      As to why you cannot use the "rerender" attribute with the inputFile, my guess is, it is a security feature from Salesforce. Couldn't find any documentation as to why.

      Delete
  4. Nice article. It was very useful for me.

    ReplyDelete
  5. I am facing same problem but i m not getting how to solve??
    @Terece

    ReplyDelete
  6. HI
    I am having a list of inputfile to upload file, it is not working as with error "apex:inputFile can not be used in conjunction with an action component, apex:commandButton or apex:commandLink that specifies a rerender or oncomplete attribute. "

    Can you please help how to resolve. Thanks

    ReplyDelete
    Replies
    1. Hi Sirai,

      in the above article there is a section I call Inputfile - gotcha which explains the problem you are having above and what a possible Workaround is. Here is that section. Read it and implement the work around. Cheers

      Inputfile Gotcha
      If you use Inputfile component and Attempt to use Visualforce Ajax rerender attribute or the oncomplete attribute you will run into this nasty stuff here.

      apex:inputFile can not be used in conjunction with an action component, apex:commandButton or apex:commandLink that specifies a rerender or oncomplete attribute

      As the error says, rerender and oncomplete are just not allowed together with inputFile component. Most of the time, you will have to do a full page load each time which can cause performance issues because you lose your amazing Ajax capabilities. But as i said, - most of the time. Read on.

      Tip – Workaround
      In Visualforce there is a component call actionRegion. When you wrap a block within the actionRegion Tag, what you are doing is telling the Controller to consider only the piece of code wrapped by the actionRegion and ignore everything else. The name says it all, actionRegion(region or action). If you have a region you wish to rerender, that does not have anything to do with the inputfile enclosing it in an actionRegion will make sure that the Controller does not even know that an inputFile is on your page. This is an example on how to use this;








      This technique will however not work, if you rerender the portion with the inputFile maybe to dynamically add more upload files like I did in the post, with dynamically adding fields for more email recipients. Checkout it out here.

      Delete
    2. I see the example was copied. Please refer to that section in the article. Cheers

      Delete
  7. hi,

    i have a problem in controller class,my requirement is to fetch all the attachments from objects to visual force page and in vf page by selecting checkbox of a particular attachment and click on save button it get to be saved in an object. can u please give me controller class.

    ReplyDelete