[轉]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 個問題。倘若不使用 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

 

-------------------------------------------------------------------------------------------------

ASP.NET 上傳進度顯示 - 補遺 (Update 04/16)

 

下列是ASP.NET上傳機制的補遺.

 

1. UploadFrameHelper 提供了GetPreviousPage函式,可以取得Cross-Page PostBack時
   的Page,如下所示:
  

<%@ 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();
            System.Web.UI.WebControls.FileUpload fl =   (System.Web.UI.WebControls.FileUpload)p.FindControl("FileUpload1");
            fl.SaveAs(@"c:/uploadfile"+fl.FileName);
            context.Response.Write(context.Request.Files[0].FileName);           
        }
    }
 
    public bool IsReusable {
        get {
            return false;
        }
    }

}


2. 這個機制事實上並不受限於maxRequestLength的設定,
   所謂的限制來自於下方的程式碼.
  
   HackUpload.cs
  
 ..........
   public bool Load()
    {
        if (_context.Request.ContentLength < GetMaxRequestSize())
        {
   ..........

     若移除此處程式碼,那麼上傳檔案將無限制,我猜大概只受限於executionTimeout及
   IO,大概是2GB 或 4GB吧.
  
3.ASP.NET Runtime 的記憶體耗費,記憶中, ASP.NET Runtime是將上傳的檔案先行存在記憶體中的,

 這意味著當上傳檔案很大,而且有很多人使用時,IIS將會耗掉許多記憶體.

 從追蹤ASP.NET Runtime的處理機制來看,我想會有辦法解決此問題,不過得待我有空時才能好好看看.

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章