Calling Response.TransmitFile() from stati

2019-07-03 00:57发布

问题:

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:

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);  
    }
}


回答2:

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!
}


回答3:

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?



回答4:

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.



回答5:

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);