Saturday 22 January 2011

Uploading Multiple Attachments via Visualforce

I've lost count of the number of times I've implemented an attachments page in Visualforce, sometimes for a single file, sometimes for a finite number and once or twice for an indeterminate number.  Following the principles of DRY, I decided to create a custom component that would allow me to attach files to any specified parent sobject with Add More functionality that allowed me to upload several files at once.  As its easy to forget what attachments you have already uploaded, I also wanted the existing attachments to be listed. Below is a screenshot of the completed component:



The component has a mandatory attribute named "sobjId" - this is the id of the sobject that the uploaded files will be attached to. Note that you can't use the name "id" for the attribute, as this is a standard attribute that allows the component to be identified by other components in the page.

The Attachment sobjects backing the input file elements are stored in a list in the controller that is initialised in the constructor to contain a single element.  Clicking the Add More button causes another five Attachment sobjects to be appended to the list.  As the user can choose to fill in the attachments in any order (e.g. just populating the last two elements), we can't simply insert the attachments when the save button is clicked, as that will attempt to insert the empty attachments that the user hasn't supplied details for.  Thus we iterate the list and only insert those Attachments that have the Body field populated.

Note that there is no field for the user to input the name of the Attachment - I'm using the filename for the this, as the filename extension gives a browser a good chance of automatically opening the attachment when it is downloaded.

The Done button simply takes the user back to the view page for the object matching the supplied id attribute.

There's a couple of wrinkles when dealing with file uploads:

  • When the page initially renders with one file chooser, if you choose a file and then click the Add More button, the chooser will not remember the file that you chose.  This is browser behaviour, as it is not possible to prefill a file chooser under any circumstances - otherwise malicious pages could upload files from your hard disk without your consent.
  • The body of the uploaded file is stored as a Blob.  To unit test code that handles file uploading, you can create a Blob from a String using the valueof method:

    Blob bodyBlob=Blob.valueOf('Unit Test Attachment Body');
    
The code (including unit tests!) and Visualforce can be downloaded here.  Simply unzip into the src directory of your org in the Force.com IDE.

Note that when using the example page, you'll need to supply the id of an existing sobject (Account, Opportunity etc).

43 comments:

  1. This is amazing!! Thank you! Just what I needed.

    ReplyDelete
  2. Hey Bob, one thing you might try to avoid the filename clearing issues when adding more would be to render the page initially with a large number (say 100) apex:inputFile tags all wrapped with an output panel of with display:none;. Note that we couldn't use rendered here, because we need the file input tags to be present in the DOM. When the add more button is clicked used javascript to unhide 5 of the hidden input file panels.

    ReplyDelete
  3. Hi Bob,
    Your code was very helpful, Thanks for sharing.
    My requirement is bit more complex, when user uploads multiple attachments from a visual force page i have to insert those attachments into multiple records that are related to that particular user.

    Is there anyway i can achieve this.

    As per your code I can insert multiple attachments into single record, but i have to insert those attachments into multiple records.

    ReplyDelete
    Replies
    1. Did you get the solution to your issue I also have multiple record on the same vf page and have to attach user defined files to upload to each record.Please let me know.

      Delete
  4. You can use this code as a starting point. You'll need to change the setting up of the parent id for the attachments, so that these point to the appropriate records. You'd also need to figure out which records the files get attached to - e.g. would these be determined via code or would the user be able to choose them in some way.

    ReplyDelete
  5. hi Bob,

    When the page is submitted, salesforce checks whether the files are too big (5MB+), or if the filenames are too long (80+ chars).

    While it's not a problem getting these errors to display, how would you suggest we show which of the files generated any specific error message?

    Say, if you have 5 files, two are OK, and one has a long file name, this code will upload two files that are OK, and will have an error for the third one, but will not say which file exactly had caused an error.

    ReplyDelete
  6. Also, to clarify the problem I listed above - Salesforce checks for all those errors before any controller code is executed, and adds some default error messages to ApexPages. How can we add say filenames to those messages?

    ReplyDelete
  7. OK, figured it out:
    1) Had to use a String variable for file name, and then assign it to Attachment.name. This gets around the 80 character limit that inputFile puts on file names.
    2) Had to use a Blob variable for file body, and then assign it to Attachment.Body. This gets around SF checking the file size, and automatically ignoring files >5MB.
    3) Once the form is submitted, you will have access to all file properties via the String and Blob variables you created, and can generate detailed error messages for the user, referencing file name etc. This becomes especially convenient when you have multiple files that are being uploaded, and only some of them cause errors, but the other ones get uploaded correctly.

    Thanks for the post!

    ReplyDelete
    Replies
    1. See this other post from Bob about how to handle this (wasn't possible in 2012 but has been since advent of HTML 5): http://bobbuzzard.blogspot.com/2014/09/check-file-size-on-upload-in-visualforce.html

      Delete
  8. That's pretty much the pattern if the standard validation isn't giving you the info - bypassing it and validating yourself server side. Thanks for sharing your findings.

    ReplyDelete
  9. How do I get the sObject Id? For example I want to attach this to a lead record, how do I do that?

    ReplyDelete
  10. good post, thanks to share it!
    what about the remove link? I'm trying to do it but I can't it works...any suggest or idea?

    ReplyDelete
  11. That should be fairly straightforward.

    Add an apex:commandlink to the table with a nested apex:param to pass the attachment record id to the controller - this will have to have a rerender attribute to work - I'd use the id of the entire table. There's more detail at this blog post: http://bobbuzzard.blogspot.com/2011/07/passing-parameters-to-apex-method-from.html

    Tie this to an action method that deletes the attachment record based on the id and return null.

    ReplyDelete
  12. Thanks Bob this is very helpful. is it possible to rerender only the list of attachments after the attachment has been uploaded?

    ReplyDelete
  13. As you can't rerender when using an inputfile component, I don't think you'll be able to do this.

    ReplyDelete
  14. Hi Bob,

    How do I call the apex component to my VF page and how can I give sobjId to that of mine. What does objId refer to in here

    ReplyDelete
    Replies
    1. sobjId is the Salesforce ID of the record that you are going to associate the attachments with. For example, if you want to upload attachments to a particular account, you would set the sobjId to the account id.

      Delete
  15. Thanks for this post very useful. I am getting ** view state exceeded ** error when uploading files.
    apex:inputFile accept="PDF, TXT" attribute is not honored in some browsers, so I have use ContentType controller. this is causing the viewstate exceeded error.

    When I use this code as is I am not getting any error. any suggestions on how to get around this error?

    ReplyDelete
    Replies
    1. @sfdummy - I ran into the same issue when branching Bob's code. The issue is that if you use the file chooser to select a file and then cause a page refresh without doing anything with the blob, it'll serialize the file itself into the viewstate and unless it's a really small file, this will throw you over the viewstate governor limit.

      Here's a post that talks about it: http://developer.force.com/cookbook/recipe/uploading-a-document-using-visualforce-and-a-custom-controller

      Here's the relevant note:
      Make sure you take a look at the finally block in the controller code above. The finally block always executes when the try block exits regardless if an error occurs or not. You need to ensure you clear out document’s body (document.body = null) so that the blob is not automatically included in the serialized image of the controller. If you do not clear out the body, you’ll get the following view state error: "Maximum view state size limit (128K) exceeded. Actual viewstate size for this page was…"

      Delete
  16. Great post Bob! Kudos

    ReplyDelete
  17. Hi can you send the code for Uploading Multiple Attachments via Visualforce

    ReplyDelete
    Replies
    1. You can download it from the link in the post.

      Delete
  18. hi bob great job...I want how to upload multiple attachments into customobject

    ReplyDelete
    Replies
    1. Custom objects are no different to standard objects with regard to attachments. The attachment records simply need the id of the custom object as their parentid.

      Delete
  19. hi bob can u provide code for about screen shot my mail id:sreenathyadavcloud@gmail.com

    ReplyDelete
    Replies
    1. There's a link at the bottom of the post to the Visualforce, apex and unit tests.

      Delete
    2. Hey Bob, thanks for sharing this. How we attach the screenshots to a Standard object record during create since we do not have the ParentID yet?

      Delete
  20. Great tool! We were using a free one from the appexchange, but it's no longer supported by any browser, and was always sketchy on mobile devices.

    This one works great, especially since there's no java, flash or CSS.

    Here are a few things I had to do for my org:
    1.) add a line to the vf component under the button commads so my users wouldn't kill me for not warning them: NOTE: If you need to upload more than 6 files, click "Add More" button before you start browsing for files; the action reloads the page and doesn't remember what you've already selected.

    2.) Separate the test class out, since in-line testing is no longer admitted. Don't forget to re-declare the static variable: public static final Integer NUM_ATTACHMENTS_TO_ADD=5;

    3.) Create a button to use on our Opp page (URL): /apex/MultiAttachment?id={!Opportunity.Id}

    Use the merge field picklist in the button creation page to replace opp id when adding to different objects.

    4.) Immediately add 5 browse fields upon loading: add "addMore()" to the method called when loading.
    public MultiAttachmentController()
    {
    // instantiate the list with a single attachment
    newAttachments=new List{new Attachment()};
    addMore();
    }

    If you do this, you'll need to update the test class:

    System.assertEquals(1, controller.newAttachments.size());
    needs to be
    System.assertEquals(6, controller.newAttachments.size());

    and

    System.assertEquals(1 + NUM_ATTACHMENTS_TO_ADD, controller.newAttachments.size());
    needs to be
    System.assertEquals(6 + NUM_ATTACHMENTS_TO_ADD, controller.newAttachments.size());

    ReplyDelete
    Replies
    1. Whoops, the formatting for the vf component note didn't take:
      it needs to be a footer facet: apex:facet name="footer"

      Delete
  21. i want similar output on the contact detail page
    what should i do for it
    what approach should i keep in my controller coding

    ReplyDelete
  22. Have you tried to do this for salesforce classic?

    ReplyDelete
  23. Thank you very much for this code. One issue is that the previously selected values gets lost once a new row is added . Anyway around to retain the prev selected values ?

    ReplyDelete
  24. This comment has been removed by the author.

    ReplyDelete
  25. This is a very good post Bob, but my requirement is to add multiple attachments on an Send Email Page. Here the Task Record is not saved and does not have the taskId with which I need to associate the attachments. Is there an easy way to reuse your scripts or how else can I invoke the attachment picker which allows to associated multiple attachments from a Send Email VF Page.

    ReplyDelete
  26. Thanks for this post! I always find your answers helpful when you comment on the issues I'm working on. If I google and I see your name on the results, I know I'm going to get an answer that works for me.

    ReplyDelete
  27. Awesome.. Works like a charm.. Thanks a lot for ur post and for sharing ur knowledge.

    ReplyDelete
  28. Awesome.. Works like a charm.. Thanks a lot for ur post and for sharing ur knowledge.

    ReplyDelete
  29. So Bob, forgive me, but I'm a newbie. If I want to attach the files to an account (integrating this VF page into the account detail page), how do I jigger the apex attribute description in the controller? Thanks!

    ReplyDelete
  30. HI , Where can i see the code. The functionality is really helpful.

    ReplyDelete
  31. Hi Bob , I hav eone question is there any way that we can prevent page refresh becuase we lost all attachment once we click on Add More. Also , i know do not work with rerender so can you suggest me any workaround ?

    ReplyDelete
  32. Hi Bob,

    Can't we get the file choosen already when we click on add more?

    ReplyDelete
  33. Hi bob,

    Awsome work. One issue is that the previously selected values gets lost once a new row is added . Anyway around to retain the prev selected values ?

    ReplyDelete
  34. Hi Bob,

    This works perfectly. However my whole page refreshes when I click "add more". Any idea how to solve this?

    ReplyDelete