0

I would like to upload an attachment greater than 135kb (view state limit) through Visualforce and link it to a new object record being creating via a custom controller. My understanding is that I need to use JSForce in order to prevent exceeding the view state limit.

My question is how would you link it to the record that is being created via the Apex custom controller?

2

Your understanding is incorrect. You don't necessarily need JavaScript to work around the limit, and further, you wouldn't need jsforce if you're in Salesforce already (e.g. a Visualforce page). If you don't expect to exceed the 6MB heap limit, you can simply mark the blob as transient:

public transient Blob fileBody { get; set; }

If you need to deal with files larger than 6MB, no Apex is going to do it for you, you'll need to use the REST API. To link the file to the appropriate record, use the ParentId field for an attachment, or use the ContentDocumentLink object if you upload a ContentDocument (recommended for new development).


Edit: Full REST API Demo

public class myPageController {
    public Id contentVersionId { get; set; }
    public PageReference goToNextPage() {
        PageReference ref = Page.nextWizardPage;
        ref.getParameters().put('contentVersionId',contentVersionId);
        return ref;
    }
}

<apex:page controller="myPageController">
    <!-- we use this form to redirect after passing the value to the controller -->
    <apex:form>
        <!-- reRender must be specified, or a full page refresh will occur, and Bad Things may happen -->
        <apex:actionFunction name="redirect" action="{!goToNextPage}" reRender="">
            <!-- "value" is supplied in JavaScript, must be blank here -->
            <!-- Ideally, name should not match assignTo, and should not be blank -->
            <!-- assignTo will assign the first positional parameter to that value in Apex. Huzzah! -->
            <apex:param name="contentVersionIdentifier" assignTo="{!contentVersionId}" value="" />
        </apex:actionFunction>
    </apex:form>
    <script>
    const sessionId = "{!$Api.Session_Id}"; // Our session Id in Visualforce, should be good for REST API calls
    const restHost = '{!URLFOR("/", null)}'; // Borrowed from https://salesforce.stackexchange.com/a/263394 as a way to find host name
    function uploadFile() {
        const theFile = document.querySelector("input[name='file']").files[0];
        // We pass in the properties as a JSON object. Add additional fields as you need:
        // ContentVersion: https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_contentversion.htm
        const headerData = JSON.stringify({
            PathOnClient: theFile.name 
        });
        // 1: file parts, 2: file name (don't use here), 3: Content Type, must be application/json, not text/json
        const headerFile = new File([headerData], "", { type: "application/json" });
        // Simulates a multipart/form, which we need
        const formData = new FormData();
        // This is the ContentVersion non-binary data
        formData.append("entity_content", headerFile);
        // And this is the binary data for VersionData. Note we use the File directly from input.files. Very important. Handle with care.
        formData.append("VersionData", theFile);
        let req = new XMLHttpRequest();
        // We do not want credentials right this second, we handle it below.
        req.withCredentials = false;
        // POST /services/data/vXX.X/sobjects/ObjectName creates a new record, REST API
        req.open("POST",`${restHost}services/data/v52.0/sobjects/ContentVersion`);
        // We will use our Visualforce API token to call the REST API
        req.setRequestHeader("Authorization", "Bearer "+sessionId);
        // After successful load, we can move on
        // Not shown: error handling. Make sure you handle onerror and onabort.
        req.onload = function() {
            let data = JSON.parse(req.responseText);
            redirect(data.id);
        }
        // Send request to the server
        req.send(formData);
    }
    </script>
    <!-- this is a normal, non apex:form; we want control over the elements -->
    <form>
        <input type="file" name="file" />
        <!-- return false to prevent default event propagation, which would submit the form and terminate the upload -->
        <button onclick="uploadFile(); return false">
            Upload
        </button>
    </form>
</apex:page>

Finally, and this is important, you need to go to Setup and configure CORS for your Visualforce domain, or the request will be denied.

This technique allows you to handle files up to 2GB in size without using any Apex, but does consume API calls in your org. Presumably, if you can handle files that large, you can probably handle the meager number of API calls this uses (just 1).

11
  • Transient is not going to work since setters aren't called when using transient and as a result, the filebody and filename are null when saving the record and attachment. Clearing the view state is not an option either since I have multiple visualforce pages and the save button is at the last page whereas the file upload page is different. I have already tried the two above methods; the attachment was not carried over to the next page. See salesforce.stackexchange.com/questions/340714/…
    – SpicyPat
    Apr 25 at 23:14
  • @SpicyPat If you have multiple VF pages linked together, that's a problem. How are you going to carry the file along until the end? Answer: you're going to have to write a Single Page App (all in one page). Look, transient is just fine, just insert a ContentDocument, store the Id of the record you created, then later in your wizard, attach the file to a record via ContentDocumentLink.
    – sfdcfox
    Apr 26 at 2:24
  • Interesting. Is there a link where I can learn to attach the file to a record? Without exceeding the view state limit, I set setRedirect to false to preserve the view state until the end of the multi-step form. But this only works up to 135kb
    – SpicyPat
    Apr 26 at 21:06
  • @SpicyPat Once you've created your ContentDocument, you create a ContentDocumentLink when you're ready to attach the file to a record. You can even reuse the same document attached to multiple files.
    – sfdcfox
    Apr 26 at 21:08
  • Thanks. Is this using SOAP API?
    – SpicyPat
    Apr 26 at 21:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.