ASP.NET MVC:如何让浏览器打开并显示PDF而不是显示下载提示?

46

好的,我有一个生成PDF并将其返回给浏览器的操作方法。问题是,IE和Chrome浏览器都会显示下载提示框,而不是自动打开PDF文件,尽管它们知道这是什么类型的文件。但是,如果我点击一个链接来访问存储在服务器上的PDF文件,它就可以正常打开,而不会显示下载提示框。

以下是用于返回PDF的代码:

public FileResult Report(int id)
{
    var customer = customersRepository.GetCustomer(id);
    if (customer != null)
    {
        return File(RenderPDF(this.ControllerContext, "~/Views/Forms/Report.aspx", customer), "application/pdf", "Report - Customer # " + id.ToString() + ".pdf");
    }
    return null;
}

以下是从服务器返回的响应头:

HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Thu, 16 Sep 2010 06:14:13 GMT
X-AspNet-Version: 4.0.30319
X-AspNetMvc-Version: 2.0
Content-Disposition: attachment; filename="Report - Customer # 60.pdf"
Cache-Control: private, s-maxage=0
Content-Type: application/pdf
Content-Length: 79244
Connection: Close

我需要在响应中添加什么特殊内容才能让浏览器自动打开PDF吗?

非常感谢任何帮助!谢谢!


看起来像是这个问题的重复,但提问得更好。 - Frédéric
5个回答

64
Response.AppendHeader("Content-Disposition", "inline; filename=foo.pdf");
return File(...

10
这会返回重复的Content-Disposition头,Chrome会拒绝该文件。有没有办法使用File方法但不返回重复的头信息,使文件内联显示? - wilk
16
@wilk,在调用 File(...) 函数时不要将文件名作为其参数。请将文件名单独处理。 - user2320724
4
我会尝试为您翻译:我想补充一点 - 如果要强制下载,请将“inline;”改为“attachment;”。 - Paul

17

在HTTP层级,你的“Content-Disposition”头应该是“inline”,而不是“attachment”。 不幸的是,FileResult(或其派生类)并不直接支持这一点。

如果您已经在页面或处理程序中生成文档,可以直接将浏览器重定向到那里。如果这不是您想要的,您可以子类化FileResult并添加流媒体文档内联支持。

public class CustomFileResult : FileContentResult
   {
      public CustomFileResult( byte[] fileContents, string contentType ) : base( fileContents, contentType )
      {
      }

      public bool Inline { get; set; }

      public override void ExecuteResult( ControllerContext context )
      {
         if( context == null )
         {
            throw new ArgumentNullException( "context" );
         }
         HttpResponseBase response = context.HttpContext.Response;
         response.ContentType = ContentType;
         if( !string.IsNullOrEmpty( FileDownloadName ) )
         {
            string str = new ContentDisposition { FileName = this.FileDownloadName, Inline = Inline }.ToString();
            context.HttpContext.Response.AddHeader( "Content-Disposition", str );
         }
         WriteFile( response );
      }
   }

一种更简单的解决方案是不在Controller.File方法中指定文件名。这样,您将不会获得ContentDisposition头,这意味着在保存PDF时会丢失文件名提示。


我最初采用ContentDisposition帮助类的方式,后来才意识到MVC也在内部使用它,但是采用了一些技巧来正确处理UTF-8文件名。当需要编码UTF-8值时,ContentDisposition帮助类会出错。有关更多详细信息,请参见我的评论 - Frédéric

1
我遇到了同样的问题,但是在Firefox中没有任何解决方案有效,直到我改变了浏览器的选项。在“选项”窗口中,选择“应用程序”选项卡,将“便携式文档格式”更改为“在Firefox中预览”。

0

我使用以下类来获得更多 content-disposition header 的选项。

它的工作方式与 Marnix 答案 相当相似,但是它不是完全使用 ContentDisposition 类生成头部,而是调整 MVC 生成的头部(该头部符合 RFC),因为当文件名必须 utf-8 编码时,ContentDisposition 类不遵守 RFC

(最初,我在 回答其他问题时 写了这一部分,并且还有 另一个。)

using System;
using System.IO;
using System.Web;
using System.Web.Mvc;

namespace Whatever
{
    /// <summary>
    /// Add to FilePathResult some properties for specifying file name without forcing a download and specifying size.
    /// And add a workaround for allowing error cases to still display error page.
    /// </summary>
    public class FilePathResultEx : FilePathResult
    {
        /// <summary>
        /// In case a file name has been supplied, control whether it should be opened inline or downloaded.
        /// </summary>
        /// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
        public bool Inline { get; set; }

        /// <summary>
        /// Whether file size should be indicated or not.
        /// </summary>
        /// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
        public bool IncludeSize { get; set; }

        public FilePathResultEx(string fileName, string contentType) : base(fileName, contentType) { }

        public override void ExecuteResult(ControllerContext context)
        {
            FileResultUtils.ExecuteResultWithHeadersRestoredOnFailure(context, base.ExecuteResult);
        }

        protected override void WriteFile(HttpResponseBase response)
        {
            if (Inline)
                FileResultUtils.TweakDispositionAsInline(response);
            // File.Exists is more robust than testing through FileInfo, especially in case of invalid path: it does yield false rather than an exception.
            // We wish not to crash here, in order to let FilePathResult crash in its usual way.
            if (IncludeSize && File.Exists(FileName))
            {
                var fileInfo = new FileInfo(FileName);
                FileResultUtils.TweakDispositionSize(response, fileInfo.Length);
            }
            base.WriteFile(response);
        }
    }

    /// <summary>
    /// Add to FileStreamResult some properties for specifying file name without forcing a download and specifying size.
    /// And add a workaround for allowing error cases to still display error page.
    /// </summary>
    public class FileStreamResultEx : FileStreamResult
    {
        /// <summary>
        /// In case a file name has been supplied, control whether it should be opened inline or downloaded.
        /// </summary>
        /// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
        public bool Inline { get; set; }

        /// <summary>
        /// If greater than <c>0</c>, the content size to include in content-disposition header.
        /// </summary>
        /// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
        public long Size { get; set; }

        public FileStreamResultEx(Stream fileStream, string contentType) : base(fileStream, contentType) { }

        public override void ExecuteResult(ControllerContext context)
        {
            FileResultUtils.ExecuteResultWithHeadersRestoredOnFailure(context, base.ExecuteResult);
        }

        protected override void WriteFile(HttpResponseBase response)
        {
            if (Inline)
                FileResultUtils.TweakDispositionAsInline(response);
            FileResultUtils.TweakDispositionSize(response, Size);
            base.WriteFile(response);
        }
    }

    /// <summary>
    /// Add to FileContentResult some properties for specifying file name without forcing a download and specifying size.
    /// And add a workaround for allowing error cases to still display error page.
    /// </summary>
    public class FileContentResultEx : FileContentResult
    {
        /// <summary>
        /// In case a file name has been supplied, control whether it should be opened inline or downloaded.
        /// </summary>
        /// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
        public bool Inline { get; set; }

        /// <summary>
        /// Whether file size should be indicated or not.
        /// </summary>
        /// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
        public bool IncludeSize { get; set; }

        public FileContentResultEx(byte[] fileContents, string contentType) : base(fileContents, contentType) { }

        public override void ExecuteResult(ControllerContext context)
        {
            FileResultUtils.ExecuteResultWithHeadersRestoredOnFailure(context, base.ExecuteResult);
        }

        protected override void WriteFile(HttpResponseBase response)
        {
            if (Inline)
                FileResultUtils.TweakDispositionAsInline(response);
            if (IncludeSize)
                FileResultUtils.TweakDispositionSize(response, FileContents.LongLength);
            base.WriteFile(response);
        }
    }

    public static class FileResultUtils
    {
        public static void ExecuteResultWithHeadersRestoredOnFailure(ControllerContext context, Action<ControllerContext> executeResult)
        {
            if (context == null)
                throw new ArgumentNullException("context");
            if (executeResult == null)
                throw new ArgumentNullException("executeResult");
            var response = context.HttpContext.Response;
            var previousContentType = response.ContentType;
            try
            {
                executeResult(context);
            }
            catch
            {
                if (response.HeadersWritten)
                    throw;
                // Error logic will usually output a content corresponding to original content type. Restore it if response can still be rewritten.
                // (Error logic should ensure headers positionning itself indeed... But this is not the case at least with HandleErrorAttribute.)
                response.ContentType = previousContentType;
                // If a content-disposition header have been set (through DownloadFilename), it must be removed too.
                response.Headers.Remove(ContentDispositionHeader);
                throw;
            }
        }

        private const string ContentDispositionHeader = "Content-Disposition";

        // Unfortunately, the content disposition generation logic is hidden in an Mvc.Net internal class, while not trivial (UTF-8 support).
        // Hacking it after its generation. 
        // Beware, do not try using System.Net.Mime.ContentDisposition instead, it does not conform to the RFC. It does some base64 UTF-8
        // encoding while it should append '*' to parameter name and use RFC 5987 encoding. https://www.rfc-editor.org/rfc/rfc6266#section-4.3
        // And https://dev59.com/ZnNA5IYBdhLWcg3wUMDn#22221217 comment.
        // To ask for a fix: https://github.com/aspnet/Mvc
        // Other class : System.Net.Http.Headers.ContentDispositionHeaderValue looks better. But requires to detect if the filename needs encoding
        // and if yes, use the 'Star' suffixed property along with setting the sanitized name in non Star property.
        // MVC 6 relies on ASP.NET 5 https://github.com/aspnet/HttpAbstractions which provide a forked version of previous class, with a method
        // for handling that: https://github.com/aspnet/HttpAbstractions/blob/dev/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs
        // MVC 6 stil does not give control on FileResult content-disposition header.
        public static void TweakDispositionAsInline(HttpResponseBase response)
        {
            var disposition = response.Headers[ContentDispositionHeader];
            const string downloadModeToken = "attachment;";
            if (string.IsNullOrEmpty(disposition) || !disposition.StartsWith(downloadModeToken, StringComparison.OrdinalIgnoreCase))
                return;

            response.Headers.Remove(ContentDispositionHeader);
            response.Headers.Add(ContentDispositionHeader, "inline;" + disposition.Substring(downloadModeToken.Length));
        }

        public static void TweakDispositionSize(HttpResponseBase response, long size)
        {
            if (size <= 0)
                return;
            var disposition = response.Headers[ContentDispositionHeader];
            const string sizeToken = "size=";
            // Due to current ancestor semantics (no file => inline, file name => download), handling lack of ancestor content-disposition
            // is non trivial. In this case, the content is by default inline, while the Inline property is <c>false</c> by default.
            // This could lead to an unexpected behavior change. So currently not handled.
            if (string.IsNullOrEmpty(disposition) || disposition.Contains(sizeToken))
                return;

            response.Headers.Remove(ContentDispositionHeader);
            response.Headers.Add(ContentDispositionHeader, disposition + "; " + sizeToken + size.ToString());
        }
    }
}

使用示例:

public FileResult Download(int id)
{
    // some code to get filepath and filename for browser
    ...

    return
        new FilePathResultEx(filepath, System.Web.MimeMapping.GetMimeMapping(filename))
        {
            FileDownloadName = filename,
            Inline = true
        };
}

请注意,使用Inline指定文件名在Internet Explorer中无法正常工作(包括11版和Windows 10 Edge版,在某些pdf文件上进行了测试),而在Firefox和Chrome中可以正常工作。Internet Explorer将忽略文件名。对于Internet Explorer,您需要修改URL路径,这在我看来相当糟糕。请参见this answer

0

不要使用 File,而是返回 FileStreamResult

确保在最后不要把新的 FileStreamResult 封装成 File。直接返回 FileStreamResult 即可。还需要修改操作的返回类型为 FileStreamResult。


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