2010-09-16 37 views
39

Ok, vì vậy tôi có một phương thức hành động tạo ra một tệp PDF và trả về trình duyệt đó. Vấn đề là thay vì tự động mở tệp PDF, IE sẽ hiển thị lời nhắc tải xuống mặc dù nó biết loại tệp đó là gì. Chrome cũng làm như vậy. Trong cả hai trình duyệt nếu tôi nhấp vào liên kết đến tệp PDF được lưu trữ trên máy chủ, nó sẽ mở ra tốt và không bao giờ hiển thị lời nhắc tải xuống.ASP.NET MVC: Làm cách nào để trình duyệt mở và hiển thị PDF thay vì hiển thị lời nhắc tải xuống?

Đây là đoạn mã đó được gọi là để trả lại 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; 
} 

Đây là tiêu đề phản hồi từ máy chủ:

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 

Tôi có phải thêm một cái gì đó đặc biệt để đáp ứng để có được trình duyệt để mở PDF tự động?

Bất kỳ trợ giúp nào được đánh giá cao! Cảm ơn!

+0

Trông giống như một quảng cáo [this] (http://stackoverflow.com/q/3206682/1178314), nhưng được hỏi theo cách tốt hơn. –

Trả lời

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

Điều này trả về tiêu đề trùng lặp Nội dung-Định đoạt và Chrome từ chối tệp. Có cách nào để sử dụng phương pháp File nhưng trả về nội tuyến tập tin mà không có tiêu đề trùng lặp? – wilk

+12

@wilk, đừng giữ tên tệp bên trong cuộc gọi tới Tệp (...) – user2320724

+2

Tôi nghĩ rằng tôi nên thêm - để buộc chuyển đổi tải xuống "nội tuyến"; để được "tập tin đính kèm;" – Paul

17

Tiêu đề 'Nội dung-Bố trí' phải có 'nội tuyến' không 'tệp đính kèm'. Thật không may, điều đó không được hỗ trợ bởi FileResult (hoặc các lớp dẫn xuất của nó) trực tiếp.

Nếu bạn đã tạo tài liệu trong một trang hoặc trình xử lý, bạn có thể chỉ cần chuyển hướng trình duyệt ở đó. Nếu đó không phải là những gì bạn muốn, bạn có thể phân lớp FileResult và thêm hỗ trợ cho các tài liệu trực tuyến.

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

Giải pháp đơn giản hơn là không chỉ định tên tệp theo phương pháp Controller.File. Bằng cách này, bạn sẽ không nhận được tiêu đề ContentDisposition, điều này có nghĩa là bạn đã xóa gợi ý tên tệp khi lưu tệp PDF.

+0

Tôi đã đi lớp lớp trình trợ giúp ContentDisposition đầu tiên, chỉ để nhận ra MVC đang sử dụng nó trong nội bộ quá, nhưng với một số hack để xử lý đúng tên tệp utf-8. Lớp trình trợ giúp ContentDisposition làm sai khi nó phải mã hóa các giá trị utf-8. Để biết thêm chi tiết, hãy xem [nhận xét của tôi ở đây] (/ câu hỏi/1012437/sử dụng-of-content-disposition-in-an-http-response-header/22221217 # comment57484455_22221217). –

0

Tôi sử dụng các lớp sau để có thêm tùy chọn với tiêu đề bố cục nội dung.

Nó hoạt động khá giống như Marnix answer, nhưng thay vì tạo đầy đủ tiêu đề với lớp ContentDisposition, rất tiếc là không tuân theo RFC khi tên tệp phải được mã hóa utf-8, nó thay thế tiêu đề do MVC tạo tuân thủ RFC.

(Ban đầu, tôi đã viết rằng trong phần sử dụng this response to another questionanother one này.) Sử dụng

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

mẫu:

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

Lưu ý rằng việc xác định một tên tập tin với Inline sẽ không làm việc với Internet Explorer (bao gồm 11, Windows 10 Edge bao gồm, thử nghiệm với một số tập tin pdf), trong khi nó hoạt động với Firefox và Chrome. Internet Explorer sẽ bỏ qua tên tệp. Đối với Internet Explorer, bạn cần phải hack đường dẫn url của mình, điều này khá là xấu. Xem this answer.

0

Tôi gặp vấn đề tương tự, nhưng không có giải pháp nào không hoạt động trong Firefox cho đến khi tôi thay đổi Tùy chọn trình duyệt của mình. Trong Options

cửa sổ, sau đó Application Tab thay đổi Portable Document Format thành Preview in Firefox.

Các vấn đề liên quan