46 votes

ASP.NET MVC : comment faire pour que le navigateur ouvre et affiche un PDF au lieu d'afficher une invite de téléchargement ?

Ok, j'ai donc une méthode d'action qui génère un PDF et le renvoie au navigateur. Le problème est qu'au lieu d'ouvrir automatiquement le PDF, IE affiche une invite de téléchargement alors qu'il sait de quel type de fichier il s'agit. Chrome fait la même chose. Dans les deux navigateurs, si je clique sur un lien vers un fichier PDF stocké sur un serveur, il s'ouvre sans problème et n'affiche jamais d'invite au téléchargement.

Voici le code qui est appelé pour retourner le 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;
}

Voici l'en-tête de réponse du serveur :

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

Dois-je ajouter quelque chose de spécial à la réponse pour que le navigateur ouvre le PDF automatiquement ?

Toute aide est grandement appréciée ! Merci !

0 votes

On dirait que c'est un double de este mais a demandé beaucoup mieux.

64voto

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

10 votes

Cela renvoie des en-têtes Content-Disposition en double, et Chrome rejette le fichier. Existe-t-il un moyen d'utiliser la méthode File mais de renvoyer le fichier en ligne sans les en-têtes en double ?

16 votes

@wilk, ne gardez pas le nom de fichier à l'intérieur de l'appel à File(...).

4 votes

J'ai pensé ajouter - pour forcer un téléchargement, remplacer "inline ;" par "attachment ;".

17voto

Marnix van Valen Points 6197

Au niveau du protocole HTTP, votre en-tête "Content-Disposition" doit comporter "inline" et non "attachment". Malheureusement, cela n'est pas pris en charge directement par le FileResult (ou ses classes dérivées).

Si vous générez déjà le document dans une page ou un gestionnaire, vous pouvez simplement y rediriger le navigateur. Si ce n'est pas ce que vous voulez, vous pouvez sous-classer le FileResult et ajouter la prise en charge des documents en continu en ligne.

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

Une solution plus simple consiste à ne pas spécifier le nom du fichier dans la commande Controller.File méthode. De cette façon, vous n'obtiendrez pas l'en-tête ContentDisposition, ce qui signifie que vous perdrez l'indication du nom de fichier lors de l'enregistrement du PDF.

0 votes

J'ai d'abord suivi la voie de la classe d'aide ContentDisposition, pour me rendre compte que MVC l'utilisait aussi en interne, mais avec un hack pour gérer correctement les noms de fichiers utf-8. La classe d'aide ContentDisposition s'y prend mal lorsqu'elle doit encoder des valeurs utf-8. Pour plus de détails, voir mon commentaire ici .

1voto

user3243323 Points 48

J'ai eu le même problème, mais aucune des solutions n'a fonctionné en Firefox jusqu'à ce que je change les options de mon navigateur. Dans Options

fenêtre, puis Application Tab changer le Portable Document Format a Preview in Firefox .

0voto

Frederic Points 502

J'utilise les classes suivantes pour avoir plus d'options avec l'en-tête content-disposition.

Cela fonctionne comme suit Marnix répond mais, au lieu de générer entièrement l'en-tête avec l'option ContentDisposition qui, malheureusement, n'est pas conforme à RFC lorsque le nom du fichier doit être encodé en utf-8, il modifie à la place l'en-tête généré par MVC, qui est conforme à la RFC.

(A l'origine, j'ai écrit cela en partie en utilisant cette réponse à une autre question et ceci un autre .)

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

Exemple d'utilisation :

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

Notez que la spécification d'un nom de fichier avec Inline ne fonctionnera pas avec Internet Explorer (11 inclus, Windows 10 Edge inclus, testé avec certains fichiers pdf), alors qu'il fonctionne avec Firefox et Chrome. Internet Explorer ignorera le nom du fichier. Pour Internet Explorer, vous devez modifier le chemin d'accès à l'URL, ce qui est plutôt mauvais. Voir cette réponse .

0voto

Ashi Points 526

Il suffit de renvoyer un FileStreamResult au lieu de File

Et assurez-vous de ne pas envelopper votre nouveau FileStreamResult dans un File à la fin. Retournez simplement le FileStreamResult tel quel. Et vous devrez probablement modifier le type de retour de l'action en FileSteamResult.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X