coco

随心。。。随性。。。随行。。。

  博客园  ::  ::  ::  ::  :: 管理

说完了WebForm和MVC中的文件上传,就不得不说用户从服务器端下载资源了。那么今天就扯扯在WebForm和MVC中是如何实现文件下载的。说起WebForm中的文件上传,codeshark在他的博文ASP.NET实现文件下载中讲到了ASP.NET中文件下载的4种方式。当然文章主要指的是在WebForm中的实现方式。总结得相当到位,那么这篇,就先来看看MVC中文件下载的方式。然后再回过头来看看它们实现方式的关联。

Part 1 MVC中的文件下载

在mvc中微软封装好了ActionResult的诸多的实现,这使我们根据需要灵活地选择操作结果来响应用户的操作。这其中一个很重要的实现就是.NET Framework 4.0中的FileResult。它表示一个用于将二进制文件内容发送到响应的基类。FileResult类有三个实现:FileContentResult、FileStreamResult、FilePathResult。mvc中的文件下载就是依赖这三个子类。不扯闲,先来看具体实现。

 FileContentResult

public ActionResult FileDownLoad()
{
    FileStream fs = new FileStream(Server.MapPath("~/uploads/Desert.jpg"), FileMode.Open, FileAccess.Read);
    byte[] bytes = new byte[fs.Length];
    fs.Read(bytes, 0, Convert.ToInt32(bytes.Length));
    return File(bytes, "image/jpeg", "Desert.jpg");
}

 FileStreamResult

public ActionResult FileDownLoad()
{
    return File(new FileStream(Server.MapPath("~/uploads/Desert.jpg"), FileMode.Open, FileAccess.Read), "image/jpeg", "Desert.jpg");
}

 FilePathResult

public ActionResult FileDownLoad()
{
    return File(Server.MapPath("~/uploads/Desert.jpg"), "image/jpeg", "Desert.jpg");

看了上面的代码是不是感觉这似乎比codeshark文章中讲到的WebForm中的文件下载代码更简洁了?确实!那么我们同时也不禁要问:不是说mvc中的文件下载依赖于FileContentResult、FilePathResult、FileStreamResult吗,为什么这里边无一例外都是return File(......)呢?这个对于接触过mvc的你来说,相信难不倒你,F12一下就全明白了:

(图1-1)

原来File(......)方法的背后返回的是FileContentResult、FileStreamResult、FilePathResult的实例。这就不足为怪了。那么也就是说其实上面的3钟实现方式你完全可以改成如下形式:

//FilePathResult
return new FilePathResult(Server.MapPath("~/uploads/Desert.jpg"), "image/jpeg") { FileDownloadName = "Desert.jpg" };
//FileStreamResult
return new FileStreamResult(new FileStream(Server.MapPath("~/uploads/Desert.jpg"), FileMode.Open, FileAccess.Read), "image/jpeg") { FileDownloadName = "Desert.jpg" };
//FileContentResult
return new FileContentResult(System.IO.File.ReadAllBytes(Server.MapPath("~/uploads/Desert.jpg")), "image/jpeg") { FileDownloadName = "Desert.jpg" };

如果你通过ILSpay查看源码的话,你会发现其实对应的return File(......)的内部实现就是如此。

问题1:不给fileDownloadNane赋值效果会如何

回过头来再看下图1-1,我们还会发现FileContentResult、FileStreamResult、FilePathResult的下载方法,还各自对应的存在一个没有第三个参数fileDownloadNane的方法重载。那么这个方法又是用来干嘛呢?小段代码看下便知:

public ActionResult FileDownLoad()
{
    return File(Server.MapPath("~/uploads/Desert.jpg")
}

--------------------------------------------------------------------------------运行结果-------------------------------------------------------------------------------------------------------------------

通过运行结果截图一眼就能看出,此时将图片直接输出在页面上,这实现图片的显示功能。那么同时我们也能够知道,同样的这样也是可以的:

return new FilePathResult(Server.MapPath("~/uploads/Desert.jpg"), "image/jpeg") ;

问题2:加不加fileDownloadNane背后到底做了什么

当然其它两种方式同样如此。那么我们不禁要问:为什么给不给fileDownloadName赋值,实现的效果完全不一样呢?那么你肯定想到了在内部肯定做了什么来加以区分。那么就以FilePathResult为例,用ILSpy来看看究竟:

(图1-2)

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

(图1-3)

先看图1-2在FileResult类中有一个FileDownloadName公开属性,我们知道ActionResult类最终要执行子类的ExecuteResult方法,而在FileResultExecuteResult方法中,对FileDownloadName进行了非空判断。如果不为空则通过Response.AddHeader()方法向客户端浏览器发送文件。如果为空则调用子类FilePathResult重写方法WriteFile() (图1-3所示)直接向页面输出响应流。当然FileContentResult、FileStreamResult道理相同。

问题3:FileContentResult、FileStreamResult、FilePathResult文件下载的方式到底有什么不同

嗯,要回答这个问题,不用说,还是得看看它的内部实现。那么我们就一个个地来看看究竟。

图1-2和图1-3种已经看得很清楚,最终文件下载时指定了FileResultExecuteResult方法,而在ExecuteResult方法中调用了FileResult的子类的WriteFile方法。那么我只需要查看每个子类的WriteFile方法便知道了。

FileContentResult

FileStreamResult

FilePathResult

再去看看codeshark的文章ASP.NET实现文件下载,在这篇文章中他总结了文件下载的4种方式:

方式一:TransmitFile实现下载。将指定的文件直接写入 HTTP 响应输出流,而不在内存中缓冲该文件。

方式二:WriteFile实现下载,将指定的文件直接写入 HTTP 响应输出流。注意:对大型文件使用此方法时,调用此方法可能导致异常。可以使用此方法的文件大小取决于 Web 服务器的硬件配置。

方式三:WriteFile分块下载

方式四:Response.BinaryWrite()流方式下载

那么与mvc中文件下载对照一下,不难发现,FilePathResult实际上是采用的Response.TransmitFile的方式实现下载的;而FileStreamResult是采用的WriteFile分块下载;FileContentResult在文章中没有直接的对照方式,而它采用的是Response.OutputStream.Write的方式。另外Response.BinaryWrite()流的方式在mvc中没有实现。当然在msdn上给出了实现我直接贴出实现代码:

实现代码:

public class BinaryContentResult : ActionResult
{
    public BinaryContentResult()
    {
    }

    // Properties for encapsulating http headers.
    public string ContentType { get; set; }
    public string FileName { get; set; }
    public byte[] Content { get; set; }

    // The code sets the http headers and outputs the file content.
    public override void ExecuteResult(ControllerContext context)
    {
        context.HttpContext.Response.ClearContent();
        context.HttpContext.Response.ContentType = ContentType;

        context.HttpContext.Response.AddHeader("content-disposition", 
            "attachment; filename=" + FileName);

        context.HttpContext.Response.BinaryWrite(Content);
        context.HttpContext.Response.End();
    }
}

调用代码:

public ActionResult Download(string fn)
{
    // Check whether the requested file is valid.
    string pfn = Server.MapPath("~/App_Data/download/" + fn);
    if (!System.IO.File.Exists(pfn))
    {
        throw new ArgumentException("Invalid file name or file not exists!");
    }

    // Use BinaryContentResult to encapsulate the file content and return it.
    return new BinaryContentResult()
    {
        FileName = fn,
        ContentType = "application/octet-stream",
        Content = System.IO.File.ReadAllBytes(pfn)
    };
}

问题4:那么FileContentResult的Response.OutputStream.Write()和Response.WriteFile()又有什么区别昵

 关于这个问题,我查了很久,没有得到比较满翼的答案,在这里希望大神指点一二!

Part 2 WebForm中的文件下载

看看这篇文章ASP.NET实现文件下载一目了然。四种方式不多说了。到这里,不得不说MVC中的文件下载is so easy!这还得归功于微软的封装。那么问题来了,在WebForm中我们是不是因该也封装一个这样的实现,这样在以后使用的时候不用写(当然一般是copy)重复写这么写这些容易忘记的代码了昵? Of course, just do it! 网上我没有找到一个针对这个实现的封装(有大神代码可以贡献一下)。我索性自己写一个,好不好,就不说了。

1.定义抽象类

先定义一个抽象列FileDownloader,定义公共的下载行为WriteFile方法和执执行下载的Execute方法以及文件下载名_fileDownloadName。代码如下:

public abstract class FileDownloader
{
    private string _fileDownloadName;
    public FileDownloader(string contentType)
    {
        if (string.IsNullOrEmpty(contentType))
        {
            throw new ArgumentException("contentType");
        }
        this.ContentType = contentType;
    }
    public void Execute(HttpContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        HttpResponse response = context.Response;
        response.ClearContent();
        response.ContentType = this.ContentType;
        if (!string.IsNullOrEmpty(this.FileDownloadName))
        {
            context.Response.AddHeader("content-disposition",
            "attachment; filename=" + this.FileDownloadName);
        }
        this.WriteFile(context.Response);
        response.Flush();
        response.End();
    }
    protected abstract void WriteFile(HttpResponse response);
    public string ContentType { get; private set; }
    public string FileDownloadName
    {
        get
        {
            return (this._fileDownloadName ?? string.Empty);
        }
        set
        {
            this._fileDownloadName = value;
        }
    }
}

2.创建实现类

此处模拟MVC中的实现方式创建对应的WebForm中的实现方式,为了易于区分说明,我采取和MVC中同样的类名。

MVC中FileContentResult的下载方式(区别于FileStreamResult分区下载),这里实现一下:

public class FileContentResult : FileDownloader
{
    public FileContentResult(byte[] fileContents, string contentType)
        : base(contentType)
    {
        if (fileContents == null)
        {
            throw new ArgumentNullException("fileContents");
        }
        this.FileContents = fileContents;
    }
    public byte[] FileContents { get; private set; }
    protected override void WriteFile(HttpResponse response)
    {
        response.OutputStream.Write(this.FileContents, 0, this.FileContents.Length);
    }
}

 FileStreamResult(分区下载):

public class FileStreamResult : FileDownloader
{
    public FileStreamResult(Stream fileStream, string contentType)
        : base(contentType)
    {
        if (fileStream == null)
        {
            throw new ArgumentNullException("fileStream");
        }
        this.FileStream = fileStream;
    }
    public Stream FileStream { get; private set; }
    protected override void WriteFile(HttpResponse response)
    {
        Stream outputStream = response.OutputStream;
        using (this.FileStream)
        {
            byte[] buffer = new byte[4096];
            while (true)
            {
                int num = this.FileStream.Read(buffer, 0, 4096);
                if (num == 0)
                {
                    break;
                }
                outputStream.Write(buffer, 0, num);
            }
        }
    }
}

FIlePathResult:

public class FIlePathResult : FileDownloader
{
    public FIlePathResult(string fileName, string contentType)
        : base(contentType)
    {
        if (string.IsNullOrEmpty(fileName))
        {
            throw new ArgumentNullException("fileName");
        }
        this.FileName = fileName;
    }
    public string FileName { get; private set; }

    protected override void WriteFile(HttpResponse response)
    {
        response.TransmitFile(this.FileName);
    }
}

另外上面我们也采用了微软上BinaryContentResult的实现,这里同样也实现一下BinaryContentResult:

public class BinaryContentResult : FileDownloader
{
    public BinaryContentResult(byte[] fileContents, string contentType)
        : base(contentType)
    {
        if (fileContents == null)
        {
            throw new ArgumentNullException("fileContents");
        }
        this.FileContents = fileContents;
    }

    protected override void WriteFile(HttpResponse response)
    {
        response.BinaryWrite(FileContents);
    }

    public byte[] FileContents { get; private set; }
}

另外还有HttpResponse.WriteFile(net 2.0中的提出的,对大型文件使用此方法时,调用此方法可能会引发异常这里姑且称之为旧的实现方式

public class FilePathOldResult : FileDownloader
{
    public FilePathOldResult(string fileName, string contentType)
        : base(contentType)
    {
        if (string.IsNullOrEmpty(fileName))
        {
            throw new ArgumentNullException("fileName");
        }
        this.FileName = fileName;
    }
    public string FileName { get; private set; }
    protected override void WriteFile(HttpResponse response)
    {
        response.WriteFile(this.FileName);
    }

Part 3 问题开发

在文件下载中,我们可能需要自动获取文件的MIME类型,这在指定文件的下载类型时。那么如何获取文件的MIME类型呢?在Mitchell Chu的博客.NET获取文件的MIME类型(Content Type)中给出了比较好的答案。这里就不做赘述。

Part 4 The end

回过头来看还是那句话,因为MVC和WebForm都是基于ASP.NET框架,因此文件下载功能的背后还是采用了相同的组件实现。

注:由于个人技术有限,对某些概念的理解可能会存在偏差,如果你发现本文存在什么bug,请指正。谢谢!

完。

 

posted on 2014-10-28 23:25  深谷&幽兰  阅读(2007)  评论(0编辑  收藏  举报