【问题标题】:Response.BinaryWrite doesn't send file to browserResponse.BinaryWrite 不向浏览器发送文件
【发布时间】:2016-04-21 09:33:50
【问题描述】:

我正在尝试在我的 ASP.NET mVC 应用程序中创建一个 ZipArchive,虽然一切似乎都在创建正常(基于调试时的变量内容),但最终结果并未下载到用户浏览器。

我有一个 javascript 函数,当用户单击链接下载照片时会调用该函数,它将所有选定照片的 ID 发送到服务器端函数:

$(document).on('click', '#download-photos', function (e) {
    var PhotoIds = $(".chkDownloadPhoto:checked").map(function () {
        return $(this).val();
    }).get();
    $.ajax({
        url: '/Photo/Download/',
        type: 'POST',
        contentType: 'application/json',
        dataType: 'json',
        data: JSON.stringify({
            photoIds: PhotoIds
        }),
    });
});

这是服务器端功能:

public ActionResult Download(List<int> photoIds)
{
    var attachments = new List<DownloadItem>();
    foreach (int photoId in photoIds)
    {
        var Photo = db.Photos.Find(photoId);

        var image = new WebImage(Server.MapPath(Photo.imageUrl));
        attachments.Add(new DownloadItem
        {
            Data = image.GetBytes(),
            FileName = Photo.Filename
        });
    }

    using (var memoryStream = new MemoryStream())
    {
        using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
        {
            foreach (var attachment in attachments)
            {
                ZipArchiveEntry entry = archive.CreateEntry(attachment.FileName);

                using (Stream ZipFile = entry.Open())
                {
                    byte[] data = attachment.Data;
                    ZipFile.Write(data, 0, data.Length);
                }
            }
        }

        Response.Clear();

        Response.ClearContent();
        Response.ClearHeaders();

        Response.ContentType = "application/x-compressed";
        Response.AddHeader("Content-Disposition", string.Format("attachment;filename=Photos-{0}.zip; size={1}", DateTime.Now.ToString("yyyyMMdd-HHmm"), memoryStream.Length));
        Response.BinaryWrite(memoryStream.ToArray());

        Response.Flush();
        Response.End();
        return null;
    }
}

当我单击链接时,代码运行,但没有任何内容发送到浏览器,我认为这会发生在 Response.BinaryWrite 行上。我做错了什么?

【问题讨论】:

  • this answer 中所述,您应该使用 HttpResponseMessage。
  • @MiguelAlexandre,这个答案是针对 Web Api 的,而这个问题是关于 asp.net-mvc 的(不告诉哪个版本:是的,它们已合并到最新版本中)。这是mvc answer

标签: c# asp.net asp.net-mvc


【解决方案1】:

使用 MVC,您必须返回一个 ActionResult 才能响应。直接调整 http 响应不会利用 MVC 框架。

使用FileResult。在控制器内部,您有 File 处理此问题的方法。

memoryStream.Position = 0;
return File(memoryStream, 
    System.Net.Mime.MediaTypeNames.Application.Zip, 
    string.Format("Photos-{0:yyyyMMdd-HHmm}.zip", DateTime.Now));

但我怀疑错误出在您用于生成 zip 的代码中。尝试将ZipArchive 保存到磁盘进行检查。

否则,将ZipFileResult 类添加到您的项目并让它为您处理压缩。

这是我使用的类(使用 MVC 5):

using System.IO.Compression;
using System.Web;
using System.Web.Mvc;
using System;
using System.Linq;
using System.IO;
using System.Net.Mime;

namespace Whatever
{
    public class ZipFileResult : FileResult
    {
        /// <summary>
        /// Folder inside zip which will contains the files.
        /// (<c>FileDownloadName</c> without its extension will be used
        /// by default if there is more than one file in the zip.
        /// </summary>
        /// <value>
        /// <c>string.Empty</c> for not having a folder inside the zip.
        /// <c>null</c> for using <c>FileDownloadName</c> without its extension 
        /// if there is more than one file in the zip.
        /// </value>
        /// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, the 
        /// name "files" will be used instead.</remarks>
        public string ZipFolder { get; set; }

        private readonly ZipFileResultEntry[] _files;

        public ZipFileResult(params ZipFileResultEntry[] files)
            : base(MediaTypeNames.Application.Zip)
        {
            _files = files;
        }

        public ZipFileResult(params string[] filesPaths)
            : this(filesPaths == null ? null : 
                filesPaths.Select(fp => ZipFileResultEntry
                    .Create(Path.GetFileName(fp), fp)).ToArray())
        {
        }

        protected override void WriteFile(HttpResponseBase response)
        {
            // By default, response is fully buffered in memory and sent
            // once completed. On big zipped content, this would cause troubles.
            // If un-buffering response is required (<c>response.BufferOutput = 
            // false;</c>), beware, it may emit very small packets,
            // causing download time to dramatically raise. To avoid this,
            // it would then required to use a BufferedStream with a
            // reasonnable buffer size (256kb for instance).
            // http://stackoverflow.com/q/26010915/1178314
            // The BufferedStream should encapsulate response.OutputStream. 
            // PositionWrapperStream must then Dispose it (current
            // implementation will not), so long for this causing OutputStream
            // to get closed (BufferedStream do not have any option for
            // telling it not to close its underlying stream, and it is 
            // sealed...).
            using (var outputStream = 
                new PositionWrapperStream(response.OutputStream))
            using (var zip = new ZipArchive(outputStream, 
                ZipArchiveMode.Create, true))
            {
                if (_files != null)
                {
                    var archiveDir = ZipFolder ??
                        (_files.Length <= 1 ? string.Empty :
                            string.IsNullOrEmpty(FileDownloadName) ? 
                                "files" : 
                                Path.ChangeExtension(FileDownloadName, null));
                    foreach (var file in _files)
                    {
                        if (file == null)
                            continue;

                        file.WriteEntry(zip, archiveDir);
                    }
                }
            }
        }

        // Workaround bug ZipArchive requiring Position while creating.
        // Taken from http://stackoverflow.com/a/21513194/1178314
        class PositionWrapperStream : Stream
        {
            private Stream _wrapped;

            private int _pos = 0;

            public PositionWrapperStream(Stream wrapped)
            {
                _wrapped = wrapped;
            }

            public override bool CanSeek { get { return false; } }

            public override bool CanWrite { get { return true; } }

            public override bool CanRead { get { return false; } }

            public override long Position
            {
                get { return _pos; }
                set { throw new NotSupportedException(); }
            }

            public override long Length { get { return _pos; } }

            public override void Write(byte[] buffer, int offset, int count)
            {
                _pos += count;
                _wrapped.Write(buffer, offset, count);
            }

            public override void Flush()
            {
                _wrapped.Flush();
            }

            protected override void Dispose(bool disposing)
            {
                // Fcd : not closing _wrapped ourselves, MVC handle that.
                _wrapped = null;
                base.Dispose(disposing);
            }

            // all the other required methods can throw NotSupportedException
            public override int Read(byte[] buffer, int offset, int count)
            {
                throw new NotSupportedException();
            }

            public override void SetLength(long value)
            {
                throw new NotSupportedException();
            }

            public override long Seek(long offset, SeekOrigin origin)
            {
                throw new NotSupportedException();
            }
        }
    }

    public abstract class ZipFileResultEntry
    {
        /// <summary>
        /// Filename to use inside the zip.
        /// </summary>
        public string Filename { get; private set; }

        internal ZipFileResultEntry(string filename)
        {
            Filename = filename;
        }

        internal abstract void WriteEntry(ZipArchive zip, string directory);

        /// <summary>
        /// Create a file to zip in response from an uncompressed file.
        /// </summary>
        /// <param name="filename">Filename to use inside the zip.</param>
        /// <param name="path">Full path to uncompressed file on 
        /// server.</param>
        public static ZipFileResultEntry Create(string filename, string path)
        {
            return new FileSystemEntry(filename, path);
        }

        /// <summary>
        /// Create a text file to zip in response using a callback.
        /// </summary>
        /// <param name="filename">Filename to use inside the zip.</param>
        /// <param name="writer">Callback responsible of writing
        /// uncompressed file content in zip stream.</param>
        public static ZipFileResultEntry CreateText(string filename, 
            Action<StreamWriter> writer)
        {
            return new TextCallbackEntry(filename, writer);
        }

        private class FileSystemEntry : ZipFileResultEntry
        {
            private readonly string SystemPath;

            public FileSystemEntry(string filename, string path)
                : base(filename)
            {
                SystemPath = path;
            }

            internal override void WriteEntry(ZipArchive zip, string directory)
            {
                zip.CreateEntryFromFile(SystemPath, 
                    Path.Combine(directory, Filename));
            }
        }

        private class TextCallbackEntry : ZipFileResultEntry
        {
            private readonly Action<StreamWriter> Writer;

            public TextCallbackEntry(string filename, 
                Action<StreamWriter> writer)
                : base(filename)
            {
                if (writer == null)
                    throw new ArgumentNullException("writer");
                Writer = writer;
            }

            internal override void WriteEntry(ZipArchive zip, string directory)
            {
                var entry = zip.CreateEntry(Path.Combine(directory, Filename));
                using (var sw = new StreamWriter(entry.Open()))
                {
                    Writer(sw);
                }
            }
        }
    }
}

然后您可以重写您的操作:

public ActionResult Download(List<int> photoIds)
{
    var attachments = new List<string>();
    foreach (int photoId in photoIds)
    {
        var Photo = db.Photos.Find(photoId);
        attachments.Add(Server.MapPath(Photo.imageUrl));
    }

    return new ZipFileResult(attachments.ToArray())
    {
        FileDownloadName = string.Format("Photos-{0:yyyyMMdd-HHmm}.zip",
            DateTime.Now)
    };
}

旁注:出于性能原因,应避免在循环中调用 db。如果这是一个 EF 上下文,最好将其更改为(假设 Photo 具有 Id 属性作为主键):

public ActionResult Download(int[] photoIds)
{
    var attachments = new List<string>();
    foreach (var photo in db.Photos.Where(p => photoIds.Contains(p.Id)))
    {
        attachments.Add(Server.MapPath(photo.imageUrl));
    }

    return new ZipFileResult(attachments.ToArray())
    {
        FileDownloadName = string.Format("Photos-{0:yyyyMMdd-HHmm}.zip",
            DateTime.Now)
    };
}

【讨论】:

  • 很好的答案,帮助我解决了问题。尽管我不同意您的第一行,但您不必返回操作结果。 (您可以返回一个 JsonResult 作为示例)
  • @Evonet,谢谢,好吧,JsonResult 继承自 ActionResult。 (顺便说一句,在我的经验中,以一种可用的方式序列化 Json 响应中的文件是非常具有挑战性的(特别是如果你必须使用 javascript 反序列化该文件)。)
  • 有道理 - 只需对您的代码进行一条评论,就会出现 FileResultUtils 不存在的错误 - 这是一个单独的帮助程序类吗?
  • @Evonet,是的,很抱歉忘记删除它。我现在已经删除了那段代码。我将此帮助类用于其他 FileResults 所需的一些附加功能,例如允许设置下载文件名同时保持内联内容可显示,或者对于被遗忘的代码段,在失败的情况下恢复默认内容类型和处置(导致默认的 HandleErrorAttribute 没有,然后,在失败的情况下,响应只是垃圾)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-19
  • 2015-04-07
  • 1970-01-01
  • 2013-09-10
  • 1970-01-01
相关资源
最近更新 更多