views:

623

answers:

5

I have a number of pages which need to support exporting data to an Excel spreadsheet. I can generate the Excel files just fine, but I'm trying to work out how to abstract this behavior so it's easily reusable from all of the pages where I need it. My current idea is to use a static utility method, as follows:

public static void SendExcelFile(System.Web.UI.Page callingPage, string downloadFileName, List<List<string>> data, string worksheetTitle)
{
    string tempFileName = Path.GetTempFileName();

    try
    {
        // Generate file using ExcelPackage
        GenerateExcelDoc(tempFileName, data, worksheetTitle);

        callingPage.Response.AddHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
        callingPage.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        callingPage.Response.AddHeader("Content-Length", new FileInfo(tempFileName).Length.ToString());
        callingPage.Response.TransmitFile(tempFileName);
    }
    finally
    {
        //When this is removed, the method works as expected.
        if (File.Exists(tempFileName))
            File.Delete(tempFileName);  
    }
}

The click handler where I'm calling SendExcelFile looks like this:

protected void lnkExport_Click(object sender, EventArgs e)
{
    List<List<string>> dataList = GatherDataForSpreadsheet();
    Utility.SendExcelFile(this, "fileNameForDownload.xlsx", dataList, "MyReports");
}

This code works just fine as an instance method of the calling page. As a static method, though, it doesn't work at all. When I click the button that invokes this, the browser shows the loading animations indefinitely, but never prompts for a file download.

I'm very new to ASP.NET (and web programming in general), so I'm sure I'm missing something here. Could someone please explain the behavior I'm seeing, and suggest a reasonable alternative to this approach?

EDIT: If I remove the call to File.Delete() at the end, the method works as expected. Does Response.TransmitFile() do the transfer asynchronously?

EDIT 2: I just needed to call Response.Flush() before I deleted the file. See my answer below. Thanks!

+1  A: 

Try this, you can get the Request and Response directly off HttpContext.Current:

public static void SendExcelFile(string downloadFileName, List<List<string>> data, string worksheetTitle)
{
    var context = HttpContext.Current;
    string tempFolder = context.Request.PhysicalApplicationPath + "temp";
    string tempFileName = tempFolder + "tempFileName.xlsx"

    if (!Directory.Exists(tempFolder))
        Directory.CreateDirectory(tempFolder);

    // Generate file using ExcelPackage
    GenerateExcelDoc(tempFileName, data, worksheetTitle);

    context.Response.AddHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
    context.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    context.Response.AddHeader("Content-Length", new FileInfo(tempFileName).Length.ToString());
    context.Response.TransmitFile(tempFileName);

    File.Delete(tempFileName);
}

Another alternative is a base class for your pages that contains this method, that may be a much easier route. You pages don't have to inherit from System.Web.UI.Page, they can inherit from something else, like this:

public class BasePage : System.Web.UI.Page
{
    public void SendExcelFile(string downloadFileName, List<List<string>> data, string worksheetTitle)
    {
        string tempFolder =Request.PhysicalApplicationPath + "temp";
        string tempFileName = tempFolder + "tempFileName.xlsx"

        if (!Directory.Exists(tempFolder))
            Directory.CreateDirectory(tempFolder);

        // Generate file using ExcelPackage
        GenerateExcelDoc(tempFileName, data, worksheetTitle);

       Response.AddHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
       Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
       Response.AddHeader("Content-Length", new FileInfo(tempFileName).Length.ToString());
       Response.TransmitFile(tempFileName);

        File.Delete(tempFileName);
    }
}

Then in your page the class looks like:

public partial class MyPage : BasePage
{
  //Stuff!
}
Nick Craver
Approach #1 gives the same result as my code (timeout waiting for response). Using a base class looks like an easy alternative, but I'd still like to understand why approach #1 doesn't work.
Odrade
A: 

I would use this instead. The current HTTP context will be available on every page.

HttpContext.Current.Response.AddHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
HttpContext.Current.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
HttpContext.Current.Response.AddHeader("Content-Length", new FileInfo(tempFileName).Length.ToString());
HttpContext.Current.Response.TransmitFile(tempFileName);
nbushnell
Oh and use HttpContext.Current.Response.Clear(); first
nbushnell
Nick suggested this, but it doesn't seem to work. Is that because I'm making the call from a static utility class?
Odrade
What part is not working? Is it not compiling or just not giving you the output.
nbushnell
+1  A: 

We need more information - what you're doing should work.

I created a stripped-down version that just sends a copy of the calling page to the client and it works as expected:

public class Utility {
    // This just sends the client a copy of the calling page itself
    public static void SendExcelFile(Page callingPage) {
        string path = callingPage.Request.PhysicalPath;
        callingPage.Response.AddHeader("Content-Disposition", "attachment;filename=test.xls");
        callingPage.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        callingPage.Response.AddHeader("Content-Length", new FileInfo(path).Length.ToString());
        callingPage.Response.TransmitFile(path);
    }
}

Here's my calling page:

public partial class main : System.Web.UI.Page {
    protected void SendButton_Click(object sender, EventArgs e) {        
        Utility.SendExcelFile(this);
    }
}

Do you see any differences from your implementation?

Jeff Sternal
Ok, clearly I'm missing some important detail. Let me look into it for a while.
Odrade
The main difference is the File.Delete() call. If I remove that, it works normally. Does that mean Response.TransmitFile() is not synchronous?
Odrade
I just needed to call Response.Flush() before I deleted the file. Thanks for your help!
Odrade
Excellent, right on! (and D'oh!) Though how strange that it only showed up when you moved into a static method - perhaps that was just a red herring all along.
Jeff Sternal
Yeah, I think I didn't bother deleting the temp file in initial instance version. Sorry to mislead you. Thanks for your help, and good day!
Odrade
+1  A: 

At this point I'd use an HTTP debugging proxy like Fiddler to compare the HTTP sessions generated by both the working (page codebehind) and nonworking (static) versions of your code.

As an aside, you should be aware that your code as written won't work well if more than one user clicks the button at the same time -- the first user's temp file may get overwritten by the second user's file, and the second user's file may get deleted in the middle of being transmitted! Consider using Path.GetTempFileName() or a guid in the filename to ensure that each user's file is uniquely named.

Mike Powell
Agreed with Mike's comments - it's probably also worth deleting the file within a `finally` block.
Jeff Sternal
I actually had some logic to use the current timestamp to generate unique file names, but I removed it from the sample code. Path.GetTempFileName() is a good tip.
Odrade
It's also helpful if there's any concern over malicious users being able to download other people's temp files -- if your temp files are stored in ~/temp/, you'll want to make sure you've configured IIS to deny access to that folder.
Mike Powell
+2  A: 

The problem was that the temp file was being deleted before the data was sent down. I just needed to call Response.Flush() like so:

public static void SendExcelFile(System.Web.UI.Page callingPage, string downloadFileName, List<List<string>> data, string worksheetTitle)
{
    string tempFileName = Path.GetTempFileName();

    try
    {
        // Generate file using ExcelPackage
        GenerateExcelDoc(tempFileName, data, worksheetTitle);

        callingPage.Response.AddHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
        callingPage.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        callingPage.Response.AddHeader("Content-Length", new FileInfo(tempFileName).Length.ToString());
        callingPage.Response.TransmitFile(tempFileName);
        callingPage.Response.Flush()  //This is what I needed
    }
    finally
    {
        if (File.Exists(tempFileName))
            File.Delete(tempFileName);  
    }
}
Odrade
Thanks for your help everyone.
Odrade