wake-up-neo.net

ASP.NET MVC: Wie kann ich den Browser öffnen und ein Fenster anzeigen? PDF anstatt eine Download-Eingabeaufforderung anzuzeigen?

Ok, ich habe also eine Aktionsmethode, die ein PDF generiert und an den Browser zurückgibt. Das Problem ist, dass anstelle des automatischen Öffnens der PDF-Datei IE eine Eingabeaufforderung zum Herunterladen angezeigt wird, obwohl sie weiß, um welche Art von Datei es sich handelt. Chrome macht das Gleiche. Wenn ich in beiden Browsern auf einen Link zu einer PDF Datei klicke, die auf einem Server gespeichert ist, wird diese Datei geöffnet und es wird keine Download-Eingabeaufforderung angezeigt.

Hier ist der Code, der aufgerufen wird, um das PDF zurückzugeben:

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

Hier ist der Antwortheader vom Server:

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

Muss ich der Antwort etwas Besonderes hinzufügen, damit der Browser das PDF automatisch öffnet?

Jede Hilfe wird sehr geschätzt! Vielen Dank!

39
Lucifer Sam
Response.AppendHeader("Content-Disposition", "inline; filename=foo.pdf");
return File(...
57
Darin Dimitrov

Auf HTTP-Ebene sollte der Header "Content-Disposition" "Inline" und nicht "Attachment" haben. Dies wird leider nicht von FileResult (oder den abgeleiteten Klassen) direkt unterstützt.

Wenn Sie das Dokument bereits in einer Seite oder einem Handler generieren, können Sie den Browser dort einfach umleiten. Wenn Sie dies nicht möchten, können Sie das FileResult subclassieren und Unterstützung für das Inline-Streaming von Dokumenten hinzufügen.

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

Eine einfachere Lösung besteht darin, den Dateinamen in der Controller.File-Methode nicht anzugeben. Auf diese Weise erhalten Sie den ContentDisposition-Header nicht. Dies bedeutet, dass Sie beim Speichern der PDF-Datei den Hinweis auf den Dateinamen verlieren.

17

Ich verwende folgende Klassen, um mehr Optionen mit Content-Disposition-Header zu haben.

Es funktioniert ziemlich wie Marnix answer , aber anstatt den Header vollständig mit der ContentDisposition-Klasse zu generieren, die leider nicht mit RFC übereinstimmt, wenn der Dateiname utf-8-codiert werden muss, wird stattdessen eine Anpassung vorgenommen der von MVC generierte Header, der RFC entspricht.

(Ursprünglich habe ich das zum Teil geschrieben - diese Antwort auf eine andere Frage und diese eine andere .)

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

Verwendungsbeispiel:

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

Beachten Sie, dass die Angabe eines Dateinamens mit Inline nicht mit Internet Explorer (11 enthalten, Windows 10 Edge enthalten, mit einigen PDF-Dateien getestet) funktioniert, während es mit Firefox und Chrome funktioniert. Internet Explorer ignoriert den Dateinamen. Für Internet Explorer müssen Sie Ihren URL-Pfad hacken, was imo ziemlich schlecht ist. Siehe diese Antwort .

0
Frédéric

Ich hatte dasselbe Problem, aber keine der Lösungen funktionierte in Firefox , bis ich die Optionen meines Browsers geändert habe. In Options

fenster und Application Tab den Portable Document Format in Preview in Firefox.

0
sosha