1

I have a VF page in which I am displaying data to users. The data is fetched from an external system. I wish to have a download button on the VF page so that users will be able to download the data in csv format.

On researching, I found out that I can redirect to a VF page that defines contentType and the file will get downloaded automatically. The problem is that another call will need to be made on that VF page. Is there a way by which I can download the data without making another call to fetch the data?

Example Code

Controller Code

public class ExportContactsDemoController {

    public List<Contact> lstContact {set;get;}
   
    public ExportContactsDemoController(){
        lstContact = [Select Id, Name, Email, Phone From Contact Limit 10];   
    }
    
}

ShowContactsDemo - This VF page displays data to the users:

<apex:page controller="ExportContactsDemoController" cache="true">
    <apex:form id="f1">
        <head>
            <script>
            function ExportToExcel(){
                confirm('Are you sure you want to export an excel ?');
                window.open('/apex/ExportContactsDemo'); 
            }
            </script>
        </head>
        <body>
            <apex:pageBlock title="10 Contacts">
                <apex:commandButton value="Export to Excel" onclick="ExportToExcel();" oncomplete=""/>
                <apex:pageBlockTable value="{!lstContact}" var="objContact">
                    <apex:column value="{!objContact.Name}"/>
                    <apex:column value="{!objContact.Email}"/>
                    <apex:column value="{!objContact.Phone}"/>
                </apex:pageBlockTable>
            </apex:pageBlock>
        </body>
    </apex:form>
</apex:page>

ExportContactsDemo - This VF page uses the same controller as previous page and downloads the data

<apex:page controller="ExportContactsDemoController" contentType="application/vnd.ms-excel#ContactsData.xls">
    <apex:pageBlock >
        <apex:pageBlockTable value="{!lstContact}" var="objContact">
            <apex:column value="{!objContact.Name}"/>
            <apex:column value="{!objContact.Email}"/>
            <apex:column value="{!objContact.Phone}"/>
        </apex:pageBlockTable>
    </apex:pageBlock>
</apex:page>

In this example, the query present inside the controller is fired twice. Is there a way I can use the data fetched by the first query and download it without querying again?

2 Answers 2

2

Yes, you can avoid the second query, but you won't be able to open it in a new window. To do this, redirect from the first page to the second through an apex: component that can redirect via an action. When you do this, Visualforce will pass the controllers between pages and retain the View State of the page, thus not calling the controller again.

I've written a very simple example you can follow along with:

commonController.apxc

public class commonController {
    public String helloWorld { get; set; }
    public commonController() {
        // Do logic only if initializing from scratch
        if(helloWorld == null) {
            helloWorld = 'Hello World';
        }
    }
}

page1.vfp

<apex:page controller="commonController">
    {!helloWorld}
    <br />
    <apex:form>
        <apex:commandButton action="{!URLFOR($Page.page2)}" value="View as PDF" />
    </apex:form>
</apex:page>

page2.vfp

<apex:page controller="commonController" renderAs="pdf">
    {!helloWorld}
</apex:page>

I've intentionally made this a PDF so it can be as simple as possible, but you can do this sort of redirect easily for any type of content type that's supported.

You could also use a merge field for renderAs, simply changing from HTML to Excel when you change a variable, etc, although I agree that keeping the pages separate is usually a better idea.

A slightly less popular approach is to use a temporary record or file somewhere in Saleforce (e.g. a ContentVersion), which allows transfer of data that would otherwise exceed the view state of 135kb.

5
  • ThankYou for the answer! I added the line <apex:commandButton action="{!URLFOR($Page.ExportContactsDemo)}" value="Export to Excel" /> in the ShowContactsDemo VF page (Rest of the code is present in the question). I faced 2 problems. 1) After clicking the button, the complete screen turns blue within 2 seconds (Like the background color of the VF page). 2) I put a debug statement in the constructor of the controller and I found that it's executing twice. It means that the query is also executing twice. What am I doing wrong? Please guide. Thanks again!
    – Koustubh
    Jan 5, 2021 at 12:43
  • In the example given in the question, the only difference between the two VF pages is that one page has contentType="application/vnd.ms-excel#ContactsData.xls". Is there a way by which we can initially keep the contentType="" and change it to contentType="application/vnd.ms-excel#ContactsData.xls" after the button is clicked? In this case, there will be no need for another VF page.
    – Koustubh
    Jan 6, 2021 at 7:02
  • 1
    @Koustubh (a) Hmm, okay, that's interesting. I didn't realize the constructor was called again. I did some testing and fixed this (you'll see the edit in a moment). (b) Yes, you could use a merge field for contentType, as you can with most other properties. Just have the button call a method to update a variable and change the content type.
    – sfdcfox
    Jan 6, 2021 at 14:29
  • Merge field approach worked. Thank You for the guidance!
    – Koustubh
    Jan 6, 2021 at 16:36
  • I have a related question. Can you please guide me? salesforce.stackexchange.com/questions/330888/…
    – Koustubh
    Jan 11, 2021 at 8:29
1

I was able to solve the issue by initially keeping the contentType="" and changing it to contentType="application/vnd.ms-excel#report.xls" when the export button was clicked.

I used a flag to change the contentType as follows:

contentType="{!IF(isExport = true, 'application/vnd.ms-excel#report.xls', '')}"

This eliminated the use of another VF page and also eliminated the need for a second query.

Working Example

VF Page Code

<apex:page controller="ExportContactsDemoController" cache="true" contentType="{!IF(isExport = true, 'application/vnd.ms-excel#report.xls', '')}">
    <apex:form id="f1">
            <apex:pageBlock title="10 Contacts">
                <apex:commandbutton id="button" value="Export" action="{!getExport}" reRender=""/>
                <apex:pageBlockTable value="{!lstContact}" var="objContact">
                    <apex:column value="{!objContact.Name}"/>
                    <apex:column value="{!objContact.Email}"/>
                    <apex:column value="{!objContact.Phone}"/>
                </apex:pageBlockTable>
            </apex:pageBlock>
    </apex:form>
</apex:page>

Controller Code:

public class ExportContactsDemoController {

    public List<Contact> lstContact {set;get;}
    public boolean isExport{get; set;}
   
    public ExportContactsDemoController(){
        isExport= false;
        lstContact = [Select Id, Name, Email, Phone From Contact Limit 10];   
    }
    
    public PageReference getExport() {
        isExport= true;
        return null;
    }
    
}
1
  • 1
    P.S. You don't need to return a null pagereference, it's sufficient to change the action method to just: public void getExport() { isExport= true; }
    – sfdcfox
    Jan 6, 2021 at 17:23

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .