在web應用中,文件上傳是個很普遍的功能,那么今天就來小結一下asp.net中文件上傳的方式。首先我們快速來回憶一下WebForm中的文件上傳的方法。
Part 1 WebForm中的文件上傳
FileUpload服務器控件
aspx:
<div> <asp:Image ImageUrl="~/uploads/1.jpg" ID="img2" runat="server" Width="150px" Height="150px" /> <asp:FileUpload runat="server" ID="fupImage" /> <input type="button" value="上傳" id="btnSubmit" runat="server" onserverclick="btnSubmit_ServerClick" /> </div>
aspx.cs:
protected void btnSubmit_ServerClick(object sender, EventArgs e) { if (fupImage.HasFile) { Regex regex = new Regex(@".(?i:jpg|jpeg|gif|png)$"); if (regex.IsMatch(Path.GetExtension(fupImage.FileName))) { string path = AppDomain.CurrentDomain.BaseDirectory + "uploads"; if (!Directory.Exists(path)) Directory.CreateDirectory(path); string filePath = fupImage.FileName; //此處需要處理同名文件 fupImage.SaveAs(Path.Combine(path, filePath)); img2.ImageUrl = "~/uploads/" + filePath; } } }
運行結果:
Note:如果image是普通的html服務器控件,那么后台賦值就要這樣:
img1.Src = "~/uploads/" + filePath;
Html服務器控件
aspx:
<div> <asp:Image ImageUrl="~/uploads/1.jpg" ID="img2" runat="server" Width="150px" Height="150px" /> <input type="file" runat="server" id="fileimg" /> <input type="button" value="上傳" id="btnSubmit" runat="server" onserverclick="btnSubmit_ServerClick" /> </div>
aspx.cs:
if (fileimg.PostedFile.ContentLength > 0) { string fileName = Path.GetFileName(fileimg.PostedFile.FileName); Regex regex = new Regex(@".(?i:jpg|jpeg|gif|png)$"); if (regex.IsMatch(Path.GetExtension(fileName))) { string path = AppDomain.CurrentDomain.BaseDirectory + "uploads"; if (!Directory.Exists(path)) Directory.CreateDirectory(path); fileimg.PostedFile.SaveAs(Path.Combine(path, fileName)); img2.ImageUrl = "~/uploads/" + fileName; } }
運行也能實現同樣的效果。
普通的Html標簽
aspx:
<div> <asp:Image ImageUrl="~/uploads/1.jpg" ID="img2" runat="server" Width="150px" Height="150px" /> <input type="file" runat="server" id="fileimage"/> <input type="button" value="上傳" id="btnSubmit" runat="server" onserverclick="btnSubmit_ServerClick" /> </div>
aspx.cs
if (Request.Files["fileimage"].HasFile()) { string fileName = Path.GetFileName(Request.Files["fileimage"].FileName); //此處位需要處理同名文件 Regex regex = new Regex(@".(?i:jpg|jpeg|gif|png)$"); if (regex.IsMatch(Path.GetExtension(fileName))) { string path = AppDomain.CurrentDomain.BaseDirectory + "uploads"; if (!Directory.Exists(path)) Directory.CreateDirectory(path); Request.Files["fileimage"].SaveAs(Path.Combine(path, fileName)); img2.ImageUrl = "~/uploads/" + fileName; } }
Note:以上僅僅為了說明問題,故省去了對文件size的判斷。
回過頭來在看看,我們會發現如下關系:
---------------------------------------------------------------------------------------------
再來看看它們最終save的方法:
---------------------------html服務器控件------------------------------------------
---------------------------html標簽------------------------------------------------
Note:由於服務器控件FileUpload,我暫時還沒辦法通過反序列化查看到內部細節,故缺少圖片。
通過上面三種不同的標簽的實現方式,可以看出基本上都是殊途同歸。因為我們知道服務器控件只是封裝了某些東西,(雖然通過反編譯有些代碼還是查看不到)我們完全可以揣測,這種實現其實就是最終第三種方式來實現的,使我們操作起來更加方便而已,它最終還是要轉換成普通的html標簽。因此服務器控件的性能相比較而言有所損失。但是由於它會根據不同的瀏覽器生成一些樣式和腳本,因此兼容性會較好。那么拋開兼容性不說,既然它們最終的實現方式是一樣的(HttpPostedFile對象),那么我們完全可以抽象出一個共通的方法來實現,以省去每次使用它們的時候要寫不同的處理方式。一下以html控件為例:
(注1)
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
public static void Upload(string filePath)
{
var request = System.Web.HttpContext.Current.Request;
foreach (string upload in request.Files)
{
HttpPostedFile hp = request.Files[upload];
if (hp.HasFile())
{
CreateFolderIfNeeded(filePath);
string fileName = Path.GetFileName(hp.FileName); //此處位需要處理同名文件
hp.SaveAs(Path.Combine(filePath, fileName));
}
}
}
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
private static void CreateFolderIfNeeded(string path) { if (!Directory.Exists(path)) { try { Directory.CreateDirectory(path); } catch (Exception) { /*TODO: You must process this exception.*/ //throw new ArgumentException(Environment.GetResourceString("Argument_PathEmpty")); } } }
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
public static class HttpPostFileExtensions { //擴展方法必須在頂級類中定義 public static bool HasFile(this HttpPostedFile file) { return (file != null && file.ContentLength > 0) ? true : false; } }
注1:特別注意的是由於Request.Files是名稱值對的集合,而名稱正是html標簽的name屬性的值,故使用普通的html控件的時候需要給file標簽加上name屬性,否則后台無法獲取到它的值。
Part 2 MVC中的文件上傳
如果習慣了使用WebForm服務器控件開發,那么初次接觸MVC(本文以razor為例),你會發現這些服務器控件已經派不上用場了,就以文件上傳為例,我們沒辦法像以前那樣使用FileUpload愉快地拖曳來實現文件上傳了。當然了所有的ASP.NET服務器控件也好,html服務器控件也好包括MVC的Htmlhelper,這些最終都要生成普通的html標簽。而且MVC和WebForm都是基於ASP.NET平台的,那么從這2個點來說,我們在上文中最后提供的一個抽象封裝(當然這只是一個簡單的demo,它不能滿足所有的實際開發中的變化了的需求)方法按道理來說也適用於MVC,那么究竟是不是這樣呢?小段代碼為證:
@using (Html.BeginForm("Index", "Home", FormMethod.Post, new { id = "form1", enctype = "multipart/form-data" })) { <img src="/uploads/1.jpg" alt="暫無圖片" id="img1" style="width:150px;height:150px;" /> <input type='file' name='file' id='file' /> <input type="button" value="上傳" id="btnSubnit" /> } <script src="~/Scripts/jquery-1.4.1.min.js"></script> <script src="~/Scripts/jquery-form.js"></script> <script type="text/javascript"> $(document).ready(function () { $("#file").bind("change", function () { $("#form1").ajaxSubmit({ success: function (data) { $("#img1").attr("src", data.filePath); } }); }); }); </script>
---------------------------------------------------------------------請允許我丑陋的展現方式-------------------------------------------------------------------------------------
[HttpPost] public ActionResult Index(HttpPostedFileBase file/*這里的參數暫時無用*/) { string path = AppDomain.CurrentDomain.BaseDirectory + "uploads"; UpLoadFileHelper.Upload(path); var filePath = "/uploads/" + Path.GetFileName(Request.Files["file"].FileName); return Json(new { filePath = filePath }); }
---------------------------------------------------------------------運行結果---------------------------------------------------------------------------------------------------
Note:以上使用了UpLoadFileHelper.Upload只是為了說明問題。實際開發中隨着頁面需求的變化,這個實現也要進行重構,也懇請博友能夠提供完美的方案。
在WebForm中有HttpPostedFile對象,同樣的在MVC中也有HttpPostedFileBase(我不知道微軟是不是有意在名字上加以區分)。其實它們反應到上下文中是一樣的。像這樣的情況還有很多,比方說細心的你一定會發現,在Controller中取到的HttpContext是HttpContextBase而在WebForm中是HttpContext。雖然他們是兩個不同的對象,但是內部實現是一樣的(當然我沒有查閱所有的這些類似的對象,也不保證所有的這些類似對象都是同樣的實現)。那么我們就使用HttpPostedFileBase來看看MVC中如何實現單個文件的上傳的:
[HttpPost] public ActionResult Index(HttpPostedFileBase file) { if (null != file && file.ContentLength>0) { string path = AppDomain.CurrentDomain.BaseDirectory + "uploads"; if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } string fileName = string.Empty; fileName = Path.GetFileName(file.FileName); file.SaveAs(Path.Combine(path, fileName)); return Json(new { filePath = "/uploads/" + fileName, code = 1 }); } return Json(new { filePath = "請選擇需要上傳的文件", code = 0 }); }
同樣也能實現同樣的效果。
Note:由於HttpPostedFileBase是從Request.Files["name"]得到的,因此Action中HttpPostedFileBase的變量名必須保持和View中file標簽的name屬性值一致。
Part 3 問題開發
關於input type="file"的style
眾所周知,file input標簽在不同的瀏覽器中會展現不同的樣式,在實際開發中,針對這個也有很多很好的解決方案,這個可以baidu或者google(說道此,滿眼是淚,希望博友可以提供海量無需翻牆即可訪問google的網址,在此不勝感激!!!)。我在這里引用WebMagazine網站一個解決方案,只要代碼如下:
---------------------------------------------------------------------HTML標簽---------------------------------------------------------------------------------------------------
<div class="file-wrapper"> <input type="file" /> <span class="button">Choose a file</span> </div>
---------------------------------------------------------------------jQuery-----------------------------------------------------------------------------------------------------
<script src="jquery.js"></script> <script type="text/javascript"> var SITE = SITE || {}; SITE.fileInputs = function () { var $this = $(this), $val = $this.val(), valArray = $val.split('\\'), newVal = valArray[valArray.length - 1], $button = $this.siblings('.button'), $fakeFile = $this.siblings('.file-holder'); if (newVal !== '') { $button.text('File Chosen'); if ($fakeFile.length === 0) { $button.after('<span class="file-holder">' + newVal + '</span>'); } else { $fakeFile.text(newVal); } } }; $(document).ready(function () { $('.file-wrapper input[type=file]').bind('change focus click', SITE.fileInputs); }); </script>
---------------------------------------------------------------------Css---------------------------------------------------------------------------------------------------------
<style type="text/css"> .file-wrapper { position: relative; display: inline-block; overflow: hidden; cursor: pointer; } .file-wrapper input { position: absolute; top: 0; right: 0; filter: alpha(opacity=1); opacity: 0.01; -moz-opacity: 0.01; cursor: pointer; } .file-wrapper .button { color: #fff; background: #117300; padding: 4px 18px; margin-right: 5px; border-radius: 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px; display: inline-block; font-weight: bold; cursor: pointer; } .file-holder { color: #000; } </style>
關於ASP.NET中input type="file"單次請求上傳文件超過默認(ASP.NET為4M,IIS7為30M)時處理異常
在ASP.NET Web開發中,這也不是什么新問題,在這里我想既然說到了文件上傳,那么不得不在老調重彈一下,而且網上雖說對於這個問題早有定論,但是還是會看到不少對ASP.NET和IIS默認限制大小的具體說法有些混亂。測試環境為iis7+asp.net 4.0。
Situation 1:不改默認配置的情況下使用FileUpload上傳一個4.69M的文件:
Situation 2:不改默認配置的情況下使用FileUpload上傳一個40M的文件:
另外更糟糕的是甚至會出現這種情況:
從不同的錯誤頁面不難看出,asp.net 默認限制上傳大小不超過4M,運行時間不超過90s(因此會出現第三種錯誤的頁面),iis7環境下為30M,超過默認設置,IIS會認為用戶是在惡意攻擊,因此會拒絕請求。當然微軟考慮到上傳到文件的需求,因此這個問題是可以通過配置解決的。那么針對它們的解決辦法:
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Note1:關於此問題的更詳細的解決辦法,可以參考Large File Upload in IIS。
Note2:不僅如此ASP.NET和IIS對其它的諸如QueryString、Url也都有限制。比方說,當文本框輸入的字節超過2048個字節的時候同樣會引發異常。我想之所以網上關於這個問題的解讀會出現偏差可能是這於這兩者沒有仔細查看的緣故吧。我想如果是這樣的話,那么我們就需要仔細看看這兩個配置借點的相信闡述了:httpRuntime、requestFiltering。
Part 4 拓展
另外說到這么多file的問題,其實我們常常看到有些網站會有上傳進度條、圖片剪切、拖曳上傳、異步上傳等,而如果要在file基礎上實現這個,還是有點麻煩的。我們使用第三方組件來實現。這個在百度上也能找到能多既有的方案。我推薦一個能夠實現多文件和進度條上傳的組件jQuery Multiple File Upload with Progress bar Tutorial。文件上傳的jQuery File Upload Demo以及jQuery File Upload。唉,好的插件確實是太多了,看得我眼花繚亂,不禁要感嘆,再也不用擔心文件上傳了。根據自己的需要選擇吧。
Part 5 參閱鏈接
http://www.codeproject.com/Articles/442515/Uploading-and-returning-files-in-ASP-NET-MVC
http://weblogs.asp.net/bryansampica/AsyncMVCFileUpload
http://ajaxuploader.com/large-file-upload-iis-asp-net.htm
http://ajaxuploader.com/large-file-upload-iis-asp-net.htm
http://msdn.microsoft.com/zh-cn/library/ms689462.aspx
Part 6 The end
注:由於個人技術有限,對某些概念的理解可能會存在偏差,如果你發現本文存在什么bug,請指正。謝謝!
完。