Silverlight以及Mvc最佳文件下载解决方案(附源码)

(一)前言

 

目前,在Silverlight中下载文件通常采用两种方式进行文件下载:

1、客户端通过SaveFileDialog类进行文件下载,服务端使用字节数组(byte[])进行数据传递。

2、客户端通过访问服务端的一般处理文件(.ashx)来进行文件下载。

 

对于第1种方式下载,缺陷主要为:点击下载之后,弹出的SaveFileDialog对话框居然没有文件名!!!(必须自己手写文件名,这里Silverlight还有待提高)。Silverlight中的SaveFileDialog相关属性和方法如下:

 1     public sealed class SaveFileDialog
 2     {
 3          public SaveFileDialog();
 4          public string DefaultExt { getset; }
 5          public string Filter { getset; }
 6          public int FilterIndex { getset; }
 7          public string SafeFileName { get; }
 8          public Stream OpenFile();
 9          public bool? ShowDialog();
10     }

 

对于第2种方式下载的话,容易暴露相关的信息(处理文件页面有时直接在地址栏显示相关的信息)。

Silverlight主要通过HtmlPage.Window.Navigate(new Uri(url));来访问一般处理文件,一般处理文件执行文件下载(Response来执行);

 

到目前为止,开发华为悍马项目已经半年多了,主要以MVC和Silverlight进行开发。因此,针对于当前的项目,本人试图以Silverlight调用Mvc action来进行下载,如下的内容都将围绕该主题进行讲解(目前这个还木有更新到项目中,仅仅是本人笔记本上设计的)。

 

(二)相关类图以及FileDownloadResult

 

 

在MVC中,Action主要以ActionResult来作为返回结果,然后调用ActionResult的ExecuteResult()方法来执行相关操作。然而,到目前为止关于文件操作的ActionResult主要为FileStreamResult、FileContentResult以及FilePathResult,这些都不太方便使用(对于文件下载来说)。因此本人打算以FileDownloadResult类来进行文件下载的相关操作。

 

(三)具体实现

 

1、FileDownloadResult类的具体实现

FileDownloadResult类主要是实现抽象类ActionResult的ExecuteResult(ControllerContext context)方法,具体代码如下:

 1     public class FileDownloadResult : ActionResult
 2     {
 3         public FileDownloadResult(string fileFullPath)
 4         {
 5             this.FileFullPath = fileFullPath;
 6         }
 7 
 8         public FileDownloadResult(string fileName, string fileFullPath)
 9         {
10             this.FileName = fileName;
11             this.FileFullPath = fileFullPath;
12         }
13 
14         public string FileName 
15         {
16             get
17             private set;
18         }
19 
20         public string FileFullPath 
21         {
22             get;
23             private set
24         }
25 
26         public override void ExecuteResult(ControllerContext context)
27         {
28             if (context == null || (!File.Exists(this.FileFullPath)))
29             {
30                 return;
31             }
32 
33             FileInfo fileInfo = new FileInfo(this.FileFullPath);
34             SetFileName(fileInfo);
35             SetResponse(context.HttpContext.Response);
36             OutputFile(context.HttpContext.Response, fileInfo);
37         }
38 
39         private void SetFileName(FileInfo fileInfo)
40         {
41             if (string.IsNullOrWhiteSpace(this.FileName))
42             {
43                 this.FileName = fileInfo.Name;
44             }
45         }
46 
47         private static void OutputFile(HttpResponseBase response, FileInfo fileInfo)
48         {
49             response.WriteFile(fileInfo.FullName, 0, fileInfo.Length);
50             response.Flush();
51             response.End();
52         }
53 
54         private void SetResponse(HttpResponseBase response)
55         {
56             SetResponseState(response);
57             SetResponseHead(response);
58             SetResponseContent(response);
59         }
60 
61         private static void SetResponseState(HttpResponseBase response)
62         {
63             response.ClearHeaders();
64             response.Clear();
65             response.Expires = 0;
66             response.Buffer = true;
67         }
68 
69         private static void SetResponseContent(HttpResponseBase response)
70         {
71             response.ContentEncoding = Encoding.UTF8;
72             response.ContentType = "Application/octet-stream";
73         }
74 
75         private void SetResponseHead(HttpResponseBase response)
76         {
77             response.HeaderEncoding = Encoding.UTF8;
78             response.AddHeader("Content-Disposition""attachment;filename=" +
79                 HttpUtility.UrlEncode(this.FileName, Encoding.UTF8).Replace("+"" "));
80         }
81     }

 

主要的要点如下:

 (1) 第28行       if (context == null || (!File.Exists(this.FileFullPath)))    ---->主要为了避免异常发生而进行的防御性编码。

 (2) 第34行       SetFileName(fileInfo);    ---->如果文件名FileName不存在,则获取文件完整路径的具体文件名称。此处主要是设置下载对话框的文件名称,可以解决Silverlight中SaveFileDialog不能设置文件名称的缺陷。具体的设置文件名称到下载对话框为如下的77-79的代码:

        77           response.HeaderEncoding = Encoding.UTF8;
        78           response.AddHeader("Content-Disposition""attachment;filename=" +
        79                 HttpUtility.UrlEncode(this.FileName, Encoding.UTF8).Replace("+"" "));
(3)第72行         response.ContentType = "Application/octet-stream";    ----->主要解决文件下载类型的问题。

 

以上的这些方法重构后代码度量的可维护性指数为81,,基本上达到代码质量的要求了。

 

2、Mvc Download Action的实现

 1     public class FileController : Controller
 2     {
 3         public ActionResult Download(string filePath)
 4         {
 5             if (!System.IO.File.Exists(filePath))
 6             { 
 7                return RedirectToAction("FileNotFound""Error");
 8             }
 9 
10             return new FileDownloadResult(filePath);
11         }
12     }


 对于文件下载,调用的方式很简单,实例化FileDownloadResult即可。

 5             if (!System.IO.File.Exists(filePath))
 6             { 
 7                return RedirectToAction("FileNotFound""Error");
 8             }
主要是对传入的文件地址的防御性的编码,对传入的空值、NULL值以及不存在的文件进行验证(后续的单元测试可以查看相关测试)。

 

3、Silverlight中访问Mvc的Download Action

1         void btnDownload_Click(object sender, RoutedEventArgs e)
2         {
3             string url = @"http://localhost:2429/File/Download?FilePath=" + "E:\\图片操作源码.txt";
4             HtmlPage.Window.Navigate(new Uri(url));
5         }

 

 4、关于Silverlight中SaveFileDialog下载,服务端获取文件字节数组的代码如下(以下的代码为本人笔记本上的代码,比项目中自己以前写的那个更简洁):

 1     public class FileHelper
 2     {
 3         public static byte[] LoadFileBytes(string fileFullName)
 4         {
 5             if (!File.Exists(fileFullName))
 6             {
 7                 return new byte[0];
 8             }
 9 
10             try
11             {
12                 return ConvertToBytes(fileFullName);
13             }
14             catch
15             {
16                 return new byte[0];
17             }
18         }
19 
20         private static byte[] ConvertToBytes(string fileFullName)
21         {
22             using (FileStream fileStream = File.OpenRead(fileFullName))
23             {
24                 return CopyToArray(fileStream);
25             }
26         }
27 
28         private static byte[] CopyToArray(FileStream fileStream)
29         {
30             using (MemoryStream memoryStream = new MemoryStream())
31             {
32                 fileStream.CopyTo(memoryStream, (int)fileStream.Length);
33                 return memoryStream.ToArray();
34             }
35         }
36     }

 

 (三)单元测试

 

1、FileDownloadResult的单元测试代码:

 1     [TestClass()]
 2     public class FileDownloadResultTest
 3     {
 4         /// <summary>
 5         /// ExecuteResult 的测试。
 6         ///</summary>
 7         [TestMethod()]
 8         public void ExecuteResultTest()
 9         {
10             string fileFullPath = @"E:\TempTestFile.txt";
11             CreateFile(fileFullPath);
12 
13             FileController controller = new FileController();
14             ExecuteResult(fileFullPath, controller);
15 
16             HttpResponseBase response=controller.ControllerContext.HttpContext.Response;
17 
18             Assert.IsNotNull(controller.ControllerContext);
19             Assert.IsNotNull(response);
20 
21             Assert.IsTrue(response.Buffer);
22             Assert.AreEqual(0, response.Expires);
23             Assert.IsTrue(string.Equals(response.ContentType, "Application/octet-stream"));
24             Assert.AreEqual(response.ContentEncoding, Encoding.UTF8);
25             Assert.AreEqual(response.HeaderEncoding, Encoding.UTF8);
26 
27             DeleteFile(fileFullPath);
28         }
29 
30         /// <summary>
31         /// 当参数异常时,ExecuteResult 的测试。
32         ///</summary>
33         [TestMethod()]
34         public void ExecuteResultWithAbnormalArgTest() 
35         {
36             string fileFullPath = @"E:\TempTestNonExsitingFile.txt";
37             FileController controller = new FileController();
38             ExecuteResult(fileFullPath, controller);
39 
40             HttpResponseBase response = controller.ControllerContext.HttpContext.Response;
41 
42             Assert.IsNotNull(controller.ControllerContext);
43             Assert.IsNotNull(response);
44         }
45 
46         /// <summary>
47         /// 当参数为null或者empty时,ExecuteResult 的测试。
48         ///</summary>
49         [TestMethod()]
50         public void ExecuteResultWithNullOrEmptyArgTest() 
51         {
52             FileController controller = new FileController();
53             ExecuteResult(null, controller);
54 
55             HttpResponseBase response = controller.ControllerContext.HttpContext.Response;
56 
57             Assert.IsNotNull(controller.ControllerContext);
58             Assert.IsNotNull(response);
59         }
60 
61         private void DeleteFile(string fileFullPath)
62         {
63             if (File.Exists(fileFullPath))
64             {
65                 File.Delete(fileFullPath);
66             }
67         }
68 
69         private void CreateFile(string fileFullPath)
70         {
71             if (!File.Exists(fileFullPath))
72             {
73                 using (FileStream fileStream = File.Create(fileFullPath))
74                 {
75                 }
76             }
77         }
78 
79         private static void ExecuteResult(string fileFullPath, FileController controller)
80         {
81             FileDownloadResult target = new FileDownloadResult(fileFullPath);
82             MvcContextHelper.SetControllerContext(controller);
83             target.ExecuteResult(controller.ControllerContext);
84         }      
85     }

 

 对于69-77行的代码,其中73-75没有做任何操作,仅仅是释放掉资源而已,避免异常的发生:

69         private void CreateFile(string fileFullPath)
70         {
71             if (!File.Exists(fileFullPath))
72             {
73                 using (FileStream fileStream = File.Create(fileFullPath))
74                 {
75                 }
76             }
77         }

 

以上的测试涉及到ControllerContext 的模拟,因此这里采用Moq来进行测试,相关代码如下:

 1     public class MvcContextHelper
 2     {
 3         public static HttpContextBase SetHttpContext()
 4         {
 5             var context = new Mock<HttpContextBase>();
 6             var request = new Mock<HttpRequestBase>();
 7             var response = new Mock<HttpResponseBase>();
 8             var session = new Mock<HttpSessionStateBase>();
 9             var server = new Mock<HttpServerUtilityBase>();
10 
11             request.Setup(r => r.Form).Returns(new NameValueCollection());
12             request.Setup(r => r.QueryString).Returns(new NameValueCollection());
13             context.Setup(ctx => ctx.Request).Returns(request.Object);
14             context.Setup(ctx => ctx.Response).Returns(response.Object);
15             context.Setup(ctx => ctx.Response.Headers).Returns(new NameValueCollection());
16             context.Setup(ctx => ctx.Session).Returns(session.Object);
17             context.Setup(ctx => ctx.Server).Returns(server.Object);
18             context.Setup(ctx => ctx.Response.Output).Returns(new StringWriter());
19 
20             return context.Object;
21         }
22 
23         public static void SetControllerContext(Controller controller)
24         {
25             var httpContext = SetHttpContext();
26             ControllerContext context = new ControllerContext(
27                 new RequestContext(httpContext, new RouteData()),
28                 controller);
29             controller.ControllerContext = context;
30         }
31     }

 

2、FileController的单元测试

 1     [TestClass()]
 2     public class FileControllerTest
 3     {
 4         /// <summary>
 5         /// Download 的测试。
 6         ///</summary>
 7         [TestMethod()]
 8         public void DownloadTest() 
 9         {
10             FileController controller = new FileController();
11             string fileFullPath = @"E:\TempTestFile.txt";
12             CreateFile(fileFullPath);
13 
14             ActionResult actionResult  = controller.Download(fileFullPath);
15 
16             FileDownloadResult result = actionResult as FileDownloadResult;
17             Assert.IsNotNull(result);
18             Assert.IsTrue(string.Equals(fileFullPath,result.FileFullPath));
19 
20             DeleteFile(fileFullPath);
21         }
22 
23         /// <summary>
24         /// 当参数异常,Download 的测试。
25         ///</summary>
26         [TestMethod()]
27         public void DownloadWithAbnormalArgTest() 
28         {
29             FileController controller = new FileController();
30             string fileFullPath = @"E:\TempTestNonExsitingFile.txt";
31             ActionResult actionResult = controller.Download(fileFullPath);
32 
33             AssertAbnormalResult(actionResult);
34         }
35 
36         /// <summary>
37         /// 当参数为null或者empty时,Download 的测试。
38         ///</summary>
39         [TestMethod()]
40         public void DownloadWithNullOrEmptyArgTest() 
41         {
42             FileController controller = new FileController();
43             ActionResult actionResult = controller.Download(string.Empty);
44             AssertAbnormalResult(actionResult);
45 
46             actionResult = controller.Download(null);
47             AssertAbnormalResult(actionResult);
48         }
49 
50         private static void AssertAbnormalResult(ActionResult actionResult)
51         {
52             RedirectToRouteResult result = actionResult as RedirectToRouteResult;
53             Assert.IsNotNull(result);
54             Assert.IsTrue(string.Equals("FileNotFound", result.RouteValues["action"]));
55             Assert.IsTrue(string.Equals("Error", result.RouteValues["controller"]));
56         }
57 
59         private void CreateFile(string fileFullPath)
60         {
61             if (!File.Exists(fileFullPath))
62             {
63                 using (FileStream fileStream = File.Create(fileFullPath))
64                 {
65                 }
66             }
67         }
68 
69         private void DeleteFile(string fileFullPath)
70         {
71             if (File.Exists(fileFullPath))
72             {
73                 File.Delete(fileFullPath);
74             }
75         }
76     }

 

(四)效果图

 

在Silverlight中点击下载显示的效果图如下:

 

(五)总结

 

上述的代码以及随笔,本人从中午吃完饭一直整到现在,XX,45行代码花了哥这么久(还有一个FileHelperTest的内容没写在随笔了,再写的话,估计全部是代码了!在源代码中有相关测试代码)。时间过得真快,还木有吃饭,自己得马山煮饭吃了,,明天又得上班了.....

 

源代码下载:  /Files/jasenkin/MVC/Jasen.MvcDownload.Web.rar

 

posted @ 2011-05-08 21:47  jasen.kin  阅读(4763)  评论(8编辑  收藏  举报