【问题标题】:ASP.NET MVC: How can I get the browser to open and display a PDF instead of displaying a download prompt?ASP.NET MVC:如何让浏览器打开并显示 PDF 而不是显示下载提示?
【发布时间】:2011-04-13 01:30:26
【问题描述】:

好的,所以我有一个生成 PDF 并将其返回给浏览器的操作方法。问题是 IE 不会自动打开 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?

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

【问题讨论】:

  • 看起来像 this 的副本,但问得更好。

标签: asp.net-mvc pdf


【解决方案1】:
Response.AppendHeader("Content-Disposition", "inline; filename=foo.pdf");
return File(...

【讨论】:

  • 这会返回重复的 Content-Disposition 标头,Chrome 会拒绝该文件。有没有办法使用 File 方法但返回内联文件而不重复标题?
  • @wilk,不要将文件名保留在对 File(...) 的调用中
  • 我想添加 - 强制下载开关“内联;”成为“附件”。
【解决方案2】:

在 HTTP 级别上,您的“Content-Disposition”标头应该具有“内联”而不是“附件”。 不幸的是,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 文件名有一些技巧。 ContentDisposition 助手类在必须编码 utf-8 值时会出错。详情请见my comment here
【解决方案3】:

我遇到了同样的问题,但在我更改浏览器的选项之前,没有一个解决方案在 Firefox 中有效。在Options

窗口,然后Application TabPortable Document Format 更改为Preview in Firefox

【讨论】:

    【解决方案4】:

    我使用以下类来获得更多带有 content-disposition 标头的选项。

    它的工作原理与Marnix answer 非常相似,但它没有使用ContentDisposition 类完全生成标头,不幸的是,当文件名必须为utf-8 编码时,它不符合RFC,而是调整了标头由 MVC 生成,符合 RFC。

    (最初,我部分地使用this response to another question 和这个another one 来编写。)

    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://stackoverflow.com/a/22221217/1178314 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 路径,这在 imo 中是相当糟糕的。见this answer

    【讨论】:

      【解决方案5】:

      只需返回 FileStreamResult 而不是 File

      并确保最后不要将新的 FileStreamResult 包装在 File 中。只需按原样返回 FileStreamResult 即可。并且可能您还需要将操作的返回类型修改为 FileSteamResult

      【讨论】:

        猜你喜欢
        • 2017-05-19
        • 2011-03-01
        • 2012-01-18
        • 2017-10-28
        • 1970-01-01
        • 2019-05-07
        • 2016-07-31
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多