摘要: ASP.NET 上傳檔案進度顯示
ASP.NET 上傳檔案進度顯示
文/黃忠成
上傳檔案所需面對的問題
運用ASP.NET的FileUpload控件來讓使用者上傳大檔案,一直以來都困擾著ASP.NET的程式設計師,雖然透過修改web.config之httpRuntime區段的maxRequestLength設定值可以讓上傳檔案的大小放大到4MB以上,但是隨之而來的問題也不少,第一個是上傳大檔案所需要花費的時間,每個ASP.NET頁面都有一個最大執行時間,一旦超過這個時間,那麼網頁就會拋出Timeout的例外,導致使用者會於瀏覽器上見到連線錯誤的網頁,這個最大執行時間預設是90秒,也就是1分鐘半,要修改這個時間,可以透過修改web.config中httpRuntime區段的executeTimeout設定來達到。第二是當使用者上傳了一個檔案大小超過限制的檔案時,ASP.NET一樣 會回應一個連線錯誤的網頁,這個網頁中根本沒有任何資訊告知使用者,錯誤是發生在使用者上傳了一個過大的檔案,這讓使用者完全弄不清楚問題出在那裡。第三個問題是上傳期間,瀏覽器會處於送出資料的狀態,使用者完全無法得知上傳的進度,此問題可透過IFrame來解決。
表1-1 ASP.NET處理大檔案上傳所需解決的問題
1、上傳大檔案所需花費的時間大於預設的1分鐘30秒。 |
2、上傳大於限制的檔案時,瀏覽器會以『連線錯誤』的網頁回應。 |
3、上傳檔案期間,網頁處於停滯狀態,使用者無從得知上傳進度。 |
4、使用者必須手動,一個個選擇要上傳的檔案。 |
檔案過大時的錯誤處理
在一月份於我的BLOG中有詳細的解法,透過IFRAME的動態顯示及隱藏功能,將連線錯誤的訊息藏起來,而後透過AJAX將易懂的訊息回報給使用者。
http://blog.csdn.net/Code6421/archive/2008/01/28/2070566.aspx
進度顯示,有可能嗎?
上傳檔案過大的錯誤顯示只是解決表1中的第二個問題,對使用者來說意義並不大,如果能解決問題3,那麼對於ASP.NET網頁上傳檔案將會有極大的改進,但有可能嗎?其實這個問題很早就有解決方案了,透過ActiveX的技巧,在上傳檔案時顯示進度並不是件難事,問題就在於,對用戶來說,安裝ActiveX控件是一個不安全的動作,更別談非IE平台上根本就沒有這東西可用了。那除了ActiveX控件外,是否還有別的解法呢?有的,你可以使用Flash類型的Upload控件,這是一勞永逸的解法,可以解決表1上所列出的4個問題。倘若不使用ActiveX、Flash,那麼這裡我將提供一個純ASP.NET AJAX的解法給各位。
要顯示檔案上傳進度,我們得先了解ASP.NET Runtime是如何處理檔案上傳的,當使用者於FileUpload控件上選擇要上傳檔案,並按下確認(Submit)按紐時,瀏覽器會送出Form上的欄位值,由於Form上有FileUpload控件,所以送出的形式會是Multipart,ASP.NET Runtime在收到這類型資料時,會依據Mutlipart中的資訊來循序讀取瀏覽器送上來的資料。也就是說,瀏覽器於送出multipart header後,就會開始送出上傳檔案的內容,而ASP.NET Runtime則於一個迴圈中不停的讀取收到的資料並解譯。
因此,如果要顯示上傳進度,我們必須要能夠插手這個收取資料迴圈,於內將進度放置Cache中,最後由AJAX Timer控件來取得資訊並使用UpdatePanel或其它機制來顯示。
問題在,這個迴圈是封閉的,一般的手法是無法對其做任何改變的,最簡單的方式是由HttpHandler開始,自行掌控關於FileUpload的所有動作,這意味著,你得自行解析multipart的資訊,而這是相當繁複的過程,至少你得讀懂RFC1341,也就是MIME中的mutlipart content type。
基於懶惰不想寫太多程式碼及除錯,我選擇了一個相當取巧的途徑,ASP.NET Runtime中本來就存在完整的multipart解譯機制,缺的只是進度回報的部份,因此我利用了Reflection機制來取用ASP.NET Runtime中的mutlipart解譯機制,並使用ASP.NET AJAX及簡易的Http Handler來完成進度回報的工作。
A Hacking
由於涉及ASP.NET Runtime中未公開的機制,我並不打算將程式碼一一列出並解釋,因為這對讀者們並沒有太大的益處(其實是連我自己都不太記得裡面的流程),取而代之的是一個簡單的範例,此例子的結構如圖1所示。
圖1
這個網站中有四個檔案,Default.aspx是顯示給使用者的上傳檔案網頁,請注意,其內內嵌了IFrame,連結至UploadHandler.aspx,而UploadHandler.aspx中的確認(Submit)按紐則是運用了Cross-Page Postback機制,將動作引導至Handler.ashx,最後由Handler.ashx呼叫HackUpload.cs中定義的Helper class來處理檔案上傳動作。
我想,其中最令人好奇的應該是HackUpload.cs的內容,在裡面處理上傳檔案的主要函式如程式1所示。
public bool Load() { if (_context.Request.ContentLength < GetMaxRequestSize()) { DateTime startTime = DateTime.Now; if (_hGetMultipartBoundary.Invoke(_context.Request, null) != null) { object ruc = CreateRawUploadContent(); HttpWorkerRequest wr = (HttpWorkerRequest)_hWorkReqeust.GetValue(_context.Request); byte[] preloadedEntityBody = wr.GetPreloadedEntityBody(); if (preloadedEntityBody != null) _hAddBytes.Invoke(ruc, new object[] { preloadedEntityBody, 0, preloadedEntityBody.Length }); if (!wr.IsEntireEntityBodyIsPreloaded()) { int num3 = (_context.Request.ContentLength > 0) ? (_context.Request.ContentLength - (int)_hLength.GetValue(ruc, null)) : 0x7fffffff; byte[] buffer = new byte[8192]; int length = (int)_hLength.GetValue(ruc, null); while (num3 > 0) { int size = buffer.Length; if (size > num3) size = num3; int num6 = wr.ReadEntityBody(buffer, size); if (num6 <= 0) break; _hreadEntityBody.SetValue(_context.Request, true); _hAddBytes.Invoke(ruc, new object[] { buffer, 0, num6 }); num3 -= num6; length += num6; OnReadProgressReport( new ReadProgressReportEventArgs( _context.Request.ContentLength, length, startTime)); } } _hdoneBytes.Invoke(ruc, null); _hrawContent.SetValue(_context.Request, ruc); } return true; } return false; } |
如你所見,這並不是一段易讀的程式碼,尤其內部牽涉到了許多ASP.NET Runtime的內部機制,這也是我決定不詳細解說此程式碼的原因。
不過用法上仍然是必須解說的,在Default.aspx中有著下列的程式碼。
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat=server > <div> <asp:ScriptManager ID="ScriptManager1" runat="server"> </asp:ScriptManager> <div> <iframe id="fileframe" name="fileframe" frameborder="0" scrolling="no" src="UploadHandler.aspx?UID=<%= UploadFrameHelper.GetUID() %>" style=" height:60px;"></iframe> <span id="statusLabel"></span> </div> <asp:UpdatePanel ID="UpdatePanel1" UpdateMode=Conditional runat="server"> <ContentTemplate> <asp:Timer ID="Timer1" Interval=500 runat="server" ontick="Timer1_Tick"> </asp:Timer> <asp:Label ID="Label1" runat="server" Text="" Visible=true></asp:Label> </ContentTemplate> </asp:UpdatePanel> </div> </form> </body> </html> |
請注意IFRAME這段,這連結到了UploadHandelr.aspx,特別的是此處呼叫了一個GetUID函式,下面是此函式的原始碼。
public static string GetUID() { if (HttpContext.Current.Session["$UPLOAD$_UID"] != null) return (string)HttpContext.Current.Session["$UPLOAD$_UID"]; HttpContext.Current.Session["$UPLOAD$_UID"] = Guid.NewGuid().ToString(); return (string)HttpContext.Current.Session["$UPLOAD$_UID"]; } |
GetUID主要的用途是在Session中產生一個識別碼,稍後我們將以此識別碼做為鍵值,在AJAX Async-postback期間,利用Cache來儲存及取得上傳進度資訊。
下面是UploadHandler.aspx的程式碼。
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="UploadHandler.aspx.cs" Inherits="UploadHandler" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <div> <script language=javascript> function delayDisable() { window.setTimeout("document.getElementById('Btn1').disabled = true;",0); window.top.setFrameVisible(false); window.top.document.getElementById("statusLabel").innerHTML = "上傳準備中,請稍後"; } </script> <asp:FileUpload ID="FileUpload1" runat="server" /> <asp:Button ID="Btn1" Text="Submit" OnClientClick="delayDisable();" runat=server /> </div> </form> </body> </html> |
文章內附的範例僅允許上傳一個檔案,如果需要上傳多個檔案,可以自行添加FileUpload控件至FileUpload.aspx內。
下面是UploadHandelr.aspx.cx的程式碼。
using System; using System.Collections; using System.Configuration; using System.Data; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; public partial class UploadHandler : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (Request.QueryString["UID"] == null) { Response.Write("invalid UID"); Response.Flush(); } else Btn1.PostBackUrl = "Handler.ashx?UID=" + Request.QueryString["UID"]; } } |
於此,我利用了Cross-Page Postback機制,將Submit動作導向Handler.ashx中,下面是.ashx的程式碼。
<%@ WebHandler Language="C#" Class="Handler" %> using System; using System.Web; using System.Reflection; using System.Security.Permissions; using System.IO; using System.Web.UI; public class Handler : IHttpHandler { public void ProcessRequest (HttpContext context) { if (UploadFrameHelper.HandleUpload()) { // 於此儲存上傳的檔案. // ie: // context.Request.Files[0].SaveAs(@"c:\temp1\upload.xxx"); //Page p = UploadFrameHelper.GetPreviousPage(); context.Response.Write( context.Request.Files[0].FileName); } } public bool IsReusable { get { return false; } } } |
請注意粗體字的部份,本文內附的範例只是於上傳檔案後顯示檔案名稱,並沒有將檔案存到硬碟中,在實際應用上,你可以呼叫context.Request.Files[0].SaveAs來儲存第一個上傳檔案至指定目錄及檔名,呼叫context.Request.Files[1].SaveAs來儲存第二個上傳檔案,以此類推。
下圖是此範例的執行畫面。
另外,此範例也整合了前篇文章所提及的檔案上傳過大的處理,讀者們可於web.config中的HttpRuntime區段修改maxRequestLength的值來限制上傳檔案的最大容量。
(PS:嫌進度列太醜嗎?呵,我的ASP.NET AJAX/Silverlight聖典一書中有漂亮點的哦。)
關於測試
這個範例及技巧,已於自身的Web Development Server、IIS及實際網路上的ASP.NET網路空間測試過,在256K上傳的頻寬,目前最大測試過上傳過300多MB,未發生任何錯誤。
為何delay.....
這個範例的完成時間是2008/1/30號,遲遲未公佈的主要原因是那時我正忙於【極意之道-.NET Framework 3.5 資料庫開發聖典 ASP.NET篇】的撰寫工作,隨著書即將於4/18號左右上市,此篇文章也沒有再拖延下去的理由了。
在公佈這篇文章時,我內心其實有些許的掙扎,原由是曾和出版社討論過另一本新書的企劃,書中將會列舉出許多有用、鮮為人知的ASP.NET手法及技巧,而此篇文章正巧可做為賣點之一,於此將其公佈,對我並沒有實質的好處,不過由於早已答應各位讀者,索性就不管了,日後若要製作該書,我再尋其它手法來取代此手法於書中的地位便是。
^_^
本文範例下載:
http://www.dreams.idv.tw/~code6421/files/UploadWithProgress2.zip