Silverlight以及Mvc最佳文件下载解决方案(附源码)
(一)前言
目前,在Silverlight中下载文件通常采用两种方式进行文件下载:
1、客户端通过SaveFileDialog类进行文件下载,服务端使用字节数组(byte[])进行数据传递。
2、客户端通过访问服务端的一般处理文件(.ashx)来进行文件下载。
对于第1种方式下载,缺陷主要为:点击下载之后,弹出的SaveFileDialog对话框居然没有文件名!!!(必须自己手写文件名,这里Silverlight还有待提高)。Silverlight中的SaveFileDialog相关属性和方法如下:
2 {
3 public SaveFileDialog();
4 public string DefaultExt { get; set; }
5 public string Filter { get; set; }
6 public int FilterIndex { get; set; }
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)方法,具体代码如下:
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的实现
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
2 {
3 string url = @"http://localhost:2429/File/Download?FilePath=" + "E:\\图片操作源码.txt";
4 HtmlPage.Window.Navigate(new Uri(url));
5 }
4、关于Silverlight中SaveFileDialog下载,服务端获取文件字节数组的代码如下(以下的代码为本人笔记本上的代码,比项目中自己以前写的那个更简洁):
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的单元测试代码:
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来进行测试,相关代码如下:
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的单元测试
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