从静态方法中调用Response.TransmitFile()

3

我有许多页面需要支持将数据导出到Excel电子表格。我可以很好地生成Excel文件,但我正在努力找出如何抽象化此行为,以便从所有需要它的页面中轻松重用。我的当前想法是使用一个静态实用方法,如下所示:

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

我调用 SendExcelFile 的点击处理程序如下:

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

这段代码作为调用页面的实例方法运行得很好。 但是作为静态方法,它根本不起作用。 当我点击调用此方法的按钮时,浏览器会无限期地显示加载动画,但从未提示文件下载。

我非常新于ASP.NET(以及Web编程),所以我确定我在这里缺少了一些东西。 有人能解释一下我看到的行为,并提出一个合理的替代方案吗?

编辑:如果我删除末尾的File.Delete()调用,则该方法按预期工作。 Response.TransmitFile()是否异步传输?

编辑2:我只需要在删除文件之前调用Response.Flush()。请参见我的答案。谢谢!


1
你在调试模式下走过代码了吗?如果是,你能告诉我它在哪里超时了(或者它是否进入了这个方法)吗?你的“SendExcelFile”方法是否抛出异常?如果是,请添加异常的堆栈跟踪,这可能有助于我们查明原因。哦,而且这个方法的一个示例调用也会有帮助的(也许是你的点击处理程序)。 - Jeff Sternal
我已经调试过了。SendExcelFile方法似乎正常执行并退出,但浏览器从未提示文件下载。IE或Firefox中的标签显示加载动画,并且浏览器底部有页面加载进度条。页面本身仍然响应。 - Odrade
如果您需要,我可以发布点击事件处理程序,但我怀疑它是否有帮助。简而言之,就是:var data = GenerateData(); Utility.SendExcelFile(data); - Odrade
Response.TransmitFile是异步的 - 我相信你需要重写HttpResponse来添加一个完成事件。 - BarrettJ
@BarrettJ - 无论 Response.TransmitFile 在底层做了什么,它都将其包装在同步 API 中。 - Jeff Sternal
显示剩余2条评论
5个回答

5
问题在于临时文件在数据发送之前被删除了。我只需要像这样调用Response.Flush():
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);  
    }
}

1

试试这个,你可以直接从HttpContext.Current获取请求(Request)响应(Response)

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

另一种选择是为您的页面创建一个包含此方法的基类,这可能是更容易的路线。 您的页面无需从System.Web.UI.Page继承,它们可以从其他东西继承,例如:

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

然后在你的页面中,这个类看起来像:

public partial class MyPage : BasePage
{
  //Stuff!
}

方法一得到的结果和我的代码一样(等待响应超时)。使用基类似乎是一个简单的替代方案,但我仍然想了解为什么方法一不起作用。 - Odrade

1

我们需要更多信息 - 您正在做的事情应该有效。

我创建了一个简化版本,只是将调用页面的副本发送给客户端,它按预期工作:

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

这是我的调用页面:
public partial class main : System.Web.UI.Page {
    protected void SendButton_Click(object sender, EventArgs e) {        
        Utility.SendExcelFile(this);
    }
}

你在你的实现中看到了什么不同吗?


好的,显然我错过了一些重要的细节。让我稍微研究一下。 - Odrade
主要的区别在于File.Delete()调用。如果我将其删除,它会正常工作。这是否意味着Response.TransmitFile()不是同步的? - Odrade
我只需要在删除文件之前调用Response.Flush()。谢谢你的帮助! - Odrade
太好了,就是这样!(还有“哎呀!”)不过很奇怪,它只在你进入静态方法时才出现 - 也许那只是一直以来的一个误导。 - Jeff Sternal
是的,我想起来了,我没有在初始版本中删除临时文件。很抱歉让你产生误解。感谢您的帮助,祝您愉快! - Odrade

1

在这个时候,我会使用像Fiddler这样的HTTP调试代理来比较由工作版本(页面代码后台)和不工作版本(静态)生成的HTTP会话。

另外,您应该知道,如果有多个用户同时点击按钮,您编写的代码将无法正常工作--第一个用户的临时文件可能会被第二个用户的文件覆盖,并且第二个用户的文件可能在传输过程中被删除!考虑使用Path.GetTempFileName()或GUID在文件名中确保每个用户的文件具有唯一名称。


同意Mike的评论- 在finally块中删除文件也是值得考虑的。 - Jeff Sternal
我实际上有一些逻辑来使用当前时间戳生成唯一的文件名,但我从示例代码中删除了它。Path.GetTempFileName()是一个好的提示。 - Odrade
如果担心恶意用户能够下载其他人的临时文件,将您的临时文件存储在~/temp/中,您需要确保已配置IIS拒绝访问该文件夹。 - Mike Powell

0

我会使用这个。当前的HTTP上下文将在每个页面上都可用。

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

噢,并且首先使用HttpContext.Current.Response.Clear(); - nbushnell
尼克建议这样做,但似乎不起作用。这是因为我从静态实用程序类中发出了调用吗? - Odrade
哪一部分出了问题?是编译不通过还是没有输出结果? - nbushnell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接