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

时间:2010-09-16 06:42:18

标签: asp.net-mvc pdf

好的,我有一个生成PDF并将其返回给浏览器的动作方法。问题是,IE不会自动打开PDF,而是显示下载提示,即使它知道它是什么类型的文件。 Chrome做同样的事情。在这两种浏览器中,如果我单击指向存储在服务器上的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?

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

5 个答案:

答案 0 :(得分:57)

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

答案 1 :(得分:17)

在HTTP级别,您的“内容处置”标题应具有“内联”而不是“附件”。 不幸的是,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时会丢失文件名提示。

答案 2 :(得分:0)

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

它与Marnix answer非常相似,但不是完全生成带有ContentDisposition类的标头,当文件名必须是utf-8编码时,遗憾的是它不符合RFC ,它调整了由MVC生成的头,它符合RFC。

(最初,我使用this response to another questionanother 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. http://tools.ietf.org/html/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,您需要破解您的网址路径,这非常糟糕。请参阅this answer

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

只需返回FileStreamResult而不是File

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