在浏览器中使用ASP.NET MVC的FileContentResult流式传输带名称的文件?

72
有没有办法使用ASP.NET MVC FileContentResult在浏览器中流式传输文件并指定名称?
我注意到你可以选择要么弹出“打开/保存”对话框或在浏览器窗口中流式传输文件,但后者会使用ActionName作为保存文件的名称。
我的场景如下:
byte[] contents = DocumentServiceInstance.CreateDocument(orderId, EPrintTypes.Quote);
result = File(contents, "application/pdf", String.Format("Quote{0}.pdf", orderId));
当我使用这个方法时,可以流式传输字节,但会向用户提供一个“打开/保存”文件对话框。我想在浏览器窗口中实际地流式传输这个文件。 如果我只是使用FilePathResult,它会在浏览器窗口中显示该文件,但是当我单击“保存”按钮以将文件保存为PDF时,它会将Action Name显示为文件名。是否有人遇到过这种情况?
7个回答

101
public ActionResult Index()
{
    byte[] contents = FetchPdfBytes();
    return File(contents, "application/pdf", "test.pdf");
}

如果要在浏览器内打开PDF,您需要设置Content-Disposition头:

public ActionResult Index()
{
    byte[] contents = FetchPdfBytes();
    Response.AddHeader("Content-Disposition", "inline; filename=test.pdf");
    return File(contents, "application/pdf");
}

3
我们采用了这种方法,这导致 MVC3 向浏览器发送了两个 Content-Disposition 标头,导致 Chrome 和 Firefox 无法显示文件。https://dev59.com/BGoy5IYBdhLWcg3wWcqh - danludwig
@Darin:如果我通过AJAX调用此控制器操作,那么AJAX中的dataType和contentType应该是什么?因为在调用此操作时,我没有得到任何文件打开框。我的PDF字节已经生成好了。 - Anil Soman
2
@AnilSoman,使用AJAX调用返回文件的控制器操作是没有意义的。如果您使用AJAX,您将永远不会得到任何文件打开框。 - Darin Dimitrov
2
@AnilSoman,很抱歉,使用AJAX是不可能实现的。您可以使用普通图像按钮提交表单,无需进行任何AJAX调用。 - Darin Dimitrov
我认为这对您应该有效:在服务器上使用Darin的代码,并在客户端上运行以下代码: window.location.href = '@Url.Action("ShowPdf")'; //或者您正在使用的其他操作的名称 - Tom
显示剩余6条评论

75

实际上,最简单的方法是按照以下方式进行...

byte[] content = your_byte[];

FileContentResult result = new FileContentResult(content, "application/octet-stream") 
{
  FileDownloadName = "your_file_name"
};

return result;

2
如果原始问题没有包含字节数组,我会同意。 - azarc3
如果你像我一样有一个文件流,除非你想将流读入数组中,否则这种方法不起作用。 - Steve Hiner
@SteveHiner 原帖中特别指出要呈现 byte[] 数据。 - azarc3

16

对于遇到此问题的人可能会有所帮助。我最终找到了解决方案。原来,即使我们使用“content-disposition”的内联方式并指定文件名,浏览器仍然不使用文件名。相反,浏览器试图根据路径/ URL解释文件名。

您可以在以下URL上阅读更多内容: 安全地在浏览器内下载文件并使用正确的文件名

这给了我一个想法,我只需创建我的URL路由,将其转换为我想要给文件的文件名并以其结尾。因此,例如,我的原始控制器调用只包含传递要打印的订单的订单ID。我期望文件名的格式为Order {0} .pdf,其中{0}是订单ID。同样,对于报价,我想要Quote {0} .pdf。

在我的控制器中,我只是添加了一个额外的参数来接受文件名称。我通过URL.Action方法将文件名作为参数传递。

然后,我创建了一个新路由,将该URL映射到以下格式: http://localhost/ShoppingCart/PrintQuote/1054/Quote1054.pdf


routes.MapRoute("", "{controller}/{action}/{orderId}/{fileName}",
                new { controller = "ShoppingCart", action = "PrintQuote" }
                , new string[] { "x.x.x.Controllers" }
            );

这段代码是关于ASP.NET MVC路由的定义。它指定了一个用于处理URL请求的路由模式。其中,花括号内的部分是参数,可以在控制器中使用。这个路由会将请求映射到具有指定控制器和操作(即动作)的处理程序上。在这个例子中,控制器名称设置为“ShoppingCart”,操作名称设置为“PrintQuote”。另外,路由定义还包含一个默认的命名空间 "x.x.x.Controllers",用于指定控制器所在的命名空间。

这基本上解决了我的问题。


一个hack,但是非常有效的hack!谢谢! - J.T. Taylor
不幸的是,仍需要在IE 11中使用。在Chrome和Firefox中不需要。 - Frédéric
Brillant 对我很有帮助...但是我发现一个陷阱,确保任何静态文件处理程序在请求到达托管代码之前不会拦截它,这是我的额外难点。例如,默认情况下,对于任何 *.jpg 的请求根本不会接近托管代码,因此甚至不会检查路由,更多信息请参见此处 - OJay

9

之前的回答是正确的:添加以下代码行...

Response.AddHeader("Content-Disposition", "inline; filename=[filename]");

...会导致多个 Content-Disposition 标头发送到浏览器。这是因为 FileContentResult 在提供文件名时会在内部应用标头。另一个相当简单的替代方案是创建 FileContentResult 的子类并覆盖其 ExecuteResult() 方法。以下是一个示例,它实例化了 System.Net.Mime.ContentDisposition 类的一个实例(与内部 FileContentResult 实现中使用的相同对象)并将其传递给新类:

public class FileContentResultWithContentDisposition : FileContentResult
{
    private const string ContentDispositionHeaderName = "Content-Disposition";

    public FileContentResultWithContentDisposition(byte[] fileContents, string contentType, ContentDisposition contentDisposition)
        : base(fileContents, contentType)
    {
        // check for null or invalid ctor arguments
        ContentDisposition = contentDisposition;
    }

    public ContentDisposition ContentDisposition { get; private set; }

    public override void ExecuteResult(ControllerContext context)
    {
        // check for null or invalid method argument
        ContentDisposition.FileName = ContentDisposition.FileName ?? FileDownloadName;
        var response = context.HttpContext.Response;
        response.ContentType = ContentType;
        response.AddHeader(ContentDispositionHeaderName, ContentDisposition.ToString());
        WriteFile(response);
    }
}

在您的控制器或基础控制器中,您可以编写一个简单的帮助程序来实例化FileContentResultWithContentDisposition,然后从操作方法中调用它,如下所示:

protected virtual FileContentResult File(byte[] fileContents, string contentType, ContentDisposition contentDisposition)
{
    var result = new FileContentResultWithContentDisposition(fileContents, contentType, contentDisposition);
    return result;
}

public ActionResult Report()
{
    // get a reference to your document or file
    // in this example the report exposes properties for
    // the byte[] data and content-type of the document
    var report = ...
    return File(report.Data, report.ContentType, new ContentDisposition {
        Inline = true,
        FileName = report.FileName
    });
}

现在,文件将以您选择的文件名发送到浏览器,并带有一个“inline; filename=[filename]”的content-disposition头。希望这可以帮助您!

我最初使用了ContentDisposition帮助类,只是意识到MVC也在内部使用它,但是需要一些技巧来正确处理utf-8文件名。当需要编码utf-8值时,ContentDisposition帮助类会出现错误。有关更多详细信息,请参见我的评论 - Frédéric
ExecuteResult不可重写,该怎么办? - Nithin Paul

7

使用ASP.NET MVC在浏览器中流式传输文件的绝对最简单方法是:

public ActionResult DownloadFile() {
    return File(@"c:\path\to\somefile.pdf", "application/pdf", "Your Filename.pdf");
}

这种方法比@azarc3建议的方法更容易,因为您甚至不需要阅读字节。
鸣谢:http://prideparrot.com/blog/archive/2012/8/uploading_and_returning_files#how_to_return_a_file_as_response **编辑**
显然我的“答案”与OP的问题相同。但我没有遇到他所遇到的问题。可能这是旧版本的ASP.NET MVC的问题?

它的问题可以抽象为“当指定文件名时,MVC发送一个带有附件属性的content-disposition头,如何将其发送为内联?测试您的解决方案响应标头,通常也会看到附件。” - Frédéric
对我来说,这就是诀窍。在Rosdi指出之前,我没想到它会这么简单。 - joseph.c

1
我用REST API在ASP.NET Core中进行了适应。
public class FileContentWithFileNameResult : FileContentResult
{
    public FileContentWithFileNameResult(byte[] fileContents, string contentType, string fileName)
   : base(fileContents, contentType)
    {
        FileName = fileName;
    }

    public string FileName { get; private set; }

    public override Task ExecuteResultAsync(ActionContext context)
    {
        var response = context.HttpContext.Response;  
        response.Headers.Append("Content-Disposition", $"inline; filename={FileName}");
        response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition");
        response.Headers.Append("X-Content-Type-Options", "nosniff");
        return base.ExecuteResultAsync(context);
    }
}

0
public FileContentResult GetImage(int productId) { 
     Product prod = repository.Products.FirstOrDefault(p => p.ProductID == productId); 
     if (prod != null) { 
         return File(prod.ImageData, prod.ImageMimeType); 
      } else { 
         return null; 
     } 
}

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