ASP.NET 上傳檔案進度顯示

摘要: ASP.NET 上傳檔案進度顯示

ASP.NET 上傳檔案進度顯示

 

 

/黃忠成

 

上傳檔案所需面對的問題

 

  運用ASP.NETFileUpload控件來讓使用者上傳大檔案,一直以來都困擾著ASP.NET的程式設計師,雖然透過修改web.confighttpRuntime區段的maxRequestLength設定值可以讓上傳檔案的大小放大到4MB以上,但是隨之而來的問題也不少,第一個是上傳大檔案所需要花費的時間,每個ASP.NET頁面都有一個最大執行時間,一旦超過這個時間,那麼網頁就會拋出Timeout的例外,導致使用者會於瀏覽器上見到連線錯誤的網頁,這個最大執行時間預設是90秒,也就是1分鐘半,要修改這個時間,可以透過修改web.confighttpRuntime區段的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個問題。倘若不使用ActiveXFlash,那麼這裡我將提供一個純ASP.NET AJAX的解法給各位。

  要顯示檔案上傳進度,我們得先了解ASP.NET Runtime是如何處理檔案上傳的,當使用者於FileUpload控件上選擇要上傳檔案,並按下確認(Submit)按紐時,瀏覽器會送出Form上的欄位值,由於Form上有FileUpload控件,所以送出的形式會是MultipartASP.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 ServerIIS及實際網路上的ASP.NET網路空間測試過,在256K上傳的頻寬,目前最大測試過上傳過300MB,未發生任何錯誤。

 

為何delay.....

 

  這個範例的完成時間是2008/1/30號,遲遲未公佈的主要原因是那時我正忙於【極意之道-.NET Framework 3.5 資料庫開發聖典 ASP.NET篇】的撰寫工作,隨著書即將於4/18號左右上市,此篇文章也沒有再拖延下去的理由了。

  在公佈這篇文章時,我內心其實有些許的掙扎,原由是曾和出版社討論過另一本新書的企劃,書中將會列舉出許多有用、鮮為人知的ASP.NET手法及技巧,而此篇文章正巧可做為賣點之一,於此將其公佈,對我並沒有實質的好處,不過由於早已答應各位讀者,索性就不管了,日後若要製作該書,我再尋其它手法來取代此手法於書中的地位便是。

 

^_^

 

本文範例下載:

http://www.dreams.idv.tw/~code6421/files/UploadWithProgress2.zip