黃忠成

風雪之閣- i live,so i writing
文章數 - 133, 回應數 - 125, 引用數 - 0


關於我:



黃忠成

  • 資深.NET 技術顧問
  • Run! PC 雜誌專欄作者
  • 程序員雜誌文章作者
  • PC Magazine 雜誌專欄作者
  • MSDN 專欄作者
  • MSDN 特約專屬講師
  • Microsoft .NET專屬講師
  • 台灣微軟特約技術顧問
  • 台灣微軟最有價值專家


  • 批評,指教,鼓勵, 請 寫信給我
    轉載文章請使用連結模式,
    請勿整篇Copy! 謝謝!


    我所提供的教育訓練:

    Windows Forms
    ASP.NET 2.0
    如有課程需要,請與我聯絡!

  • 我的著作:

  • 文章標籤

    全部標籤

    每月文章

    文章分類

    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


    DotBlogs Tags: ASP.NET

    posted on 2008/5/7 21:14 | 我要推薦 | 閱讀數 : 8919 | 文章分類 [ ASP.NET ] 訂閱

    Feedback

    # re: ASP.NET 上傳檔案進度顯示 回覆

    真的是相當實用的技巧,讓使用者可以不必漫無目的的等候,唯一的小缺憾是.NET 提供的檔案選擇元件只能選擇單一檔案^^
    2008/5/20 下午 06:45 | Joey

    # re: ASP.NET 上傳檔案進度顯示 回覆

    這篇文章讓我們這些初學者學到不少,感謝你的每篇教學文章。
    2008/7/3 下午 01:30 | bigdstut

    # re: ASP.NET 上傳檔案進度顯示 回覆

    上面的地址无法下载....
    2009/1/21 下午 05:43 | ajax

    # re: ASP.NET 上傳檔案進度顯示 回覆

    黃老师,实例无法下载
    2009/1/21 下午 05:45 | sir

    # re: ASP.NET 上傳檔案進度顯示 回覆

    老師您好,請問我要怎麼下載您的檔案呢?

    2009/6/19 上午 09:27 | joumingt

    # re: ASP.NET 上傳檔案進度顯示 回覆

    在置項中的範例檔案下載修復一文中.

    2009/6/19 下午 01:15 | code6421

    # re: ASP.NET 上傳檔案進度顯示 回覆

    to code6421 :
    我找到了,謝謝您~

    2009/6/19 下午 01:39 | joumingt

    # re: ASP.NET 上傳檔案進度顯示 回覆

    Thank you very much.
    2009/7/29 上午 11:57 | falcon

    # re: ASP.NET 上傳檔案進度顯示 回覆

    IFrame在IE8會出現警告,是屬於不安全的標籤,現在似乎已不是一個好的做法
    2009/9/11 上午 12:02 | taicomjp

    # re: ASP.NET 上傳檔案進度顯示 回覆

    如果撇開上傳檔案限制大小區塊(IE8已放大上傳檔案的大小),是可以不用iframe達到同樣效果的.

     

    2009/9/11 下午 01:15 | code6421

    # re: ASP.NET 上傳檔案進度顯示 回覆

    老師不好意思我無法下載您的範例檔案...請問還有別的載點嗎??..謝謝!!
    2009/10/12 下午 03:35 | 黃泉之月

    # re: ASP.NET 上傳檔案進度顯示 回覆

    老師,
    我也無法下載
    可以另外提供載點嗎
    如果方便寄給我的話可以寄給我嗎
    2009/11/1 下午 10:19 | Dong

    # re: ASP.NET 上傳檔案進度顯示 回覆

    sorry
    我看到載點了
    我眼殘 ><"
    2009/11/1 下午 10:30 | Dong

    回應

    標題
    姓名
    電子郵件 (將不會被顯示)
    個人網頁
    內容 
      登入後使用進階評論  
    Please add 8 and 3 and type the answer here:

    Powered by: