function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Keith987Keith987 

How to attach a PDF representation of an object as that object is created in a controller?

I have a controller - lets call it the PdfController with a PdfPage - that will create a PDF representation of an object. This controller accepts an id parameter to specify the object instance.

 

But I now have a requirement to create and attach the PDF at the time the object is created - lets call that controller the CreateController.

 

My first thought was to invoke an @future method at the end of the CreateController. (Lets ignore timing issues for now.) This would pass the object id in the PageReference parameters and then call Page.PdfPage.getContent() to get the PDF bytes and then attach them. But getContent() is documented to not work in @future methods. Presumably the issue is that the execution is asynchronous and so is essentially a separate session. There are classes like Http and HttpRequest available that could be used to create a new session and invoke on that but I have never found an example of how to establish a session to the HTML interface of Force.com from Apex and suspect that this would require a copy of the credentials even if it was possible (unless there is some session id trick possible).

 

If the Page.PdfPage.getContent() is directly invoked from the CreateController the object is not available because the transaction has not committed. And there is no explicit commit call available to force the commit beforehand.

 

So I don't think this requirement can be satisfied on Force.com but would be very happy to be corrected - please comment.

 

Thanks,

Keith

bob_buzzardbob_buzzard

Are you creating your object via a visualforce page?  If so, once you have inserted the object, the id will be available even if the transaction has not committed.   You can then create a pagereference to your PDF page, set the id parameter and execute getContent to retrieve the PDF.

 

I'm doing this very thing in order to via a Visualforce page embedded into an opportunity page layout - this creates a quote from the opportunity, generates the quote PDF and attaches it to the quote object.   

Keith987Keith987

Bob,

 

Thanks for commenting.

 

Based on your input I've done more testing but am still not seeing the object inserted by the CreateController being available in the PdfController. Below is the code stripped down (and I tried the PdfController as both a stand-alone controller and as a controller extension) and the claim count always returns zero even though the claim id is correct. So somehow my setup is different to yours; are your HTML and PDF pages using the same controller?

 

Thanks,

Keith

 

 

 

public with sharing class CreateController {
    public PageReference save() {
        cve__Claim__c claim = cve__Claim__c();
        insert claim;
        PageReference pdfPage = Page.IntakePdf;
        pdfPage.getParameters().put('id', claim.Id);
        Attachment a = new Attachment(Name = 'Intake PDF', Body = pdfPage.getContent(), ContentType = 'application/pdf', ParentId = claim.Id);
        insert a;
        PageReference claimPage = new ApexPages.StandardController(claim).view();
        claimPage.setRedirect(true);
        return claimPage;
    }
}
public with sharing PdfController {
    private Id claimId {
        get {
            if (claimId == null) {
                claimid = ApexPages.currentPage().getParameters().get('id');
            }
            System.debug('>>> IntakePdfController claimId=' + claimId +' ' + [select Count() from cve__Claim__c where Id = :claimId]);
            return claimId;
        }
        set;
    }
}

 

bob_buzzardbob_buzzard

Hi,

 

I don't use the same controller for my opportunity and quote VF pages.  The user selects a product or products to quote on and inside the controller this creates a quote in the database, instantiates the pagereference, adds a number of parameters to the parameters and gets the content of the page. The contents are then attached to a QuoteDocument object that is also created dynamically.

 

The controller for the quote VF page is an extension controller that retrieves the selected products based on id and adds in line items to the PDF.  

 

Have you tried retrieving the claim directly from its id rather than counting how many are in the database? It may be that count goes a different route that excludes uncommitted transactions (though I've not found any documentation indicating that this is the case).

Keith987Keith987

Bob,

 

Just to be clear, I was asking if you use the same controller to create the quote object and generate the quote PDF so all that work is done in the same transaction. I have one controller that creates the object and a separate controller that generates the PDF; I'm thinking I need to merge these controllers.

 

Keith

bob_buzzardbob_buzzard

No, I didn't use the same controller.

 

I have an extension controller to standard opportunity controller which inserts the quote, and then a separate extension controller to the standard quote controller which generates the document contents.  The id of the quote is passed to the page via the page reference parameters.

Keith987Keith987

Thanks. Next step for me then is to create a nice simple test case that follows your pattern; there must be something I'm doing thats getting in the way.

bob_buzzardbob_buzzard

Having just checked my VF, the page that is generating the quote it is actually using an opportunity standard controller and the quote is no longer used.  I'm fairly sure this wasn't always the case, but I can't say for certain.   Based on that it may be the the getContent call runs in a different context or similar that doesn't have access to changes made in the current transaction.  I'm digging through my other code to see if I have any other places where I've used this.

bob_buzzardbob_buzzard

I don't have anything that's doing exactly what you are, but I've knocked together a quick test using some existing pages in my dev org.

 

Page A has a custom controller and inserts an account into the database.

 

Page B has the standard account controller.

 

In my code, I create the page reference to Page B, add the newly inserted account id to the parameters and retrieve the content.  In the content retrieved from Page B I can see the name, id and description of the account that was inserted by Page A controller.

 

Based on this, your pages should be sharing the same transactional context.

Keith987Keith987

I must still be missing something because the code/pages below don't work when the "Save" button on page A is clicked as controller B doesn't find the just inserted Account row that it is passed the id of:

 

public with sharing class A {
    public PageReference save() {
        Account a = new Account(Name = 'Acme');
        insert a;
        System.debug('>>> created id=' + a.Id);
        PageReference pr = Page.B;
        pr.getParameters().put('id', a.Id);
        Blob b = pr.getContent();
        System.debug('>>> blob=' + b);
        return null;
    }
}
<apex:page controller="A">
    <apex:form>
        <apex:commandButton value="Save" action="{!save}"/>
    </apex:form>
</apex:page>
public with sharing class B {
    public Account account {
        get {
            String accountId = ApexPages.currentPage().getParameters().get('id');
            System.debug('>>> query id=' + accountId);
            // Fails on next line with "List has no rows for assignment to SObject"
            return [select Name from Account where Id = :accountId];
        }
    }
}
<apex:page controller="B" renderAs="PDF">
    <apex:outputField value="{!account.Name}"/>
</apex:page>

 

bob_buzzardbob_buzzard

I've tried your classes/pages in my dev org and I see the same behaviour that you do. After banging my head against this for half an hour I've figured out why mine works, and unfortunately its not going to help you.

 

I'm inserting my object as part of an actionmethod that is specified as the action attribute of a page.  While it appears to me that everything takes place as part of a single transaction, behind the scenes this does a server side redirect which obviously commits the first transaction and then opens another one, in which I can see my newly added account.  So it looks like the getContent/getContentAsPDF do run in some kind of unrelated context/translation.

 

I think this means that you'll need to handle this via browser techniques rather than Visualforce.  For example, as part of the save method you could set the value of a property on your controller, doAttach, to true.  Then in your page you would have some javascript which is only rendered into the page if doAttach is true, an this would invoke another action method that would grab the PDF, attach it to the new object, set doAttach to false and then head back to the page (or maybe a view page for the newly created object).  Its slightly clunky, in that the user will see the page refresh a couple of times.

 

Sorry it took so long to get this far - I'm clearly having a slow start to the week.