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
<shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f></formulas><path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></path><lock aspectratio="t" v:ext="edit"></lock></shapetype><shape id="_x0000_i1025" style="WIDTH: 375.75pt; HEIGHT: 266.25pt" o:ole="" type="#_x0000_t75"><imagedata o:title="" src="file:///C:%5CDOCUME~1%5CADMINI~1%5CLOCALS~1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_image001.png"></imagedata></shape>
這個網站中有四個檔案,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; } } } < 发表评论
最新评论
|
评论