ASP.Net 檔案上傳處理—檔案大小超過限制之處理
文/黃忠成
緣起
我於『極意之道-ASP.NET AJAX/Silverlight聖典』中放入了一個非同步擋案上傳的例子,該例利用了IFRAME技巧,巧妙的規避了AJax無法處理FileUpload控件上傳的問題,事實上此技巧在網路上已經行之有年,我只能算是應用此技巧的多數人之一。以ASP.NET頁面來處理檔案上傳,大多會遇到兩個問題,一是上傳檔案的大小大於所設下的限制時(ASP.Net預設為4M),網頁會以連線錯誤來回應(如圖1)。
圖1
二是上傳期間,網頁會處於無回應的狀態。第一個問題緣由很簡單,因為當你按下Submit按紐時,瀏覽器便會收集資料上傳,而當上傳檔案過大時,伺服器端會拒絕接收,並切斷連線,導致瀏覽器在未送完要求(Request)時,便因連線中斷而產生錯誤訊息。那有沒有可能在伺服器端預先判斷檔案大小,若超過限制時,便做出正常回應而讓原網頁顯示較友善的錯誤訊息呢?這得分成兩個部份來談,當然!在伺服器端做出判斷後回應錯誤網頁是辦的到的,但問題在於瀏覽器尚未將所有資料上傳完畢,結果還是會因為資料未完全上傳而連線被切斷而顯示出連線錯誤的訊息。那要如何解決這個問題呢 ?有個手法可以巧妙的解決此問題,就是運用IFRAME搭配AJax,利用IFRAME來裝載Upload的頁面,然後在使用者按下Submit按紐時,以Javascript將IFRAME設為不可視,接著於伺服器端判斷上傳檔案的大小,若超過限制則直接跳離,當然!此時IFRAME會出現連線錯誤的網頁 ,不過由於是處於不可視狀態,所以使用者是看不到的,此時我們可以運用JavaScript的alert來顯示錯誤訊息,或是使用Redirect來導向錯誤訊息網頁,如使用alert,那麼你必須將原來的IFRAME移除,重新建立一個,避免使用者見到那個連線錯誤的網頁。
實作
有了以上的知識,要實現就不困難了,首先要準備一個用來上傳檔案的.ASPx,程式碼如下所示。
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>
<script language="Javascript">
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
<ASP:FileUpload ID="FileUpload1" runat="server" />
<!-- 在上傳開始前,將IFrame設為不可視,以此規避因上傳檔案過大而產生的連線錯誤頁面 -->
<ASP:Button ID="Button1" OnClIEntClick="window.top.document.getElementById(''fileframe'').style.visibility = ''hidden'';" runat="server"
Text="Button" />
</div>
</form>
</body>
</Html>
UploadHandler.ASPx.cs
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;
using System.Web.Configuration;
public partial class UploadHandler : System.Web.UI.Page
{
private int GetMaxRequestSize()
{
//取得預設的maxRequestLength設定值.
Configuration config = WebConfigurationManager.OpenWebConfiguration("~");
HttpRuntimeSection section =
config.GetSection("system.web/httpRuntime") as HttpRuntimeSection;
return section.MaxRequestLength * 1024;
}
protected override System.Collections.Specialized.NameValueCollection
DeterminePostBackMode()
//此處是唯一有機會於上傳檔案之Request開始收取前,判斷大小的地方
if (Request.ContentLength > GetMaxRequestSize()) //判斷上傳檔案是否大於4 MB
{
Session["FileIsToLarge"] = true;
//利用Cache來註記檔案太大,稍後的Timer將會查詢此欄位來決定是否Redirect.
return null; //file to large
}
Session["FileIsAccept"] = true;
//接受檔案,稍後的Timer將會查詢此欄位來重新顯示IFrame
//(我們會在上傳前,將IFrame隱藏,來避開顯示錯誤.)
return base.DeterminePostBackMode();
}
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack)
{
//save file,only working at WebDev.Server
FileUpload1.SaveAs(@"c:\temp1\A" + FileUpload1.FileName);
}
}
}
此處有幾點要進一步討論,第一點是GetMaxRequestSize 函式,此函式會去抓取web.config中的maxRequestLength 設定值,來決定上傳限制。第二點是我覆載了DeterminePostBackMode函式,此函式是唯一可在Page開始收取大量上傳資料前做出判斷的地方,過了此函式,Page就會因為收取過大的資料,而產生例外。第三點,我利用了Session來儲存是否接受上傳的狀態,由主頁面利用ASP.Net AJax的Timer控件來定期查詢此值,若發現FileIsToLarge的值為Ture時,便立刻導向錯誤網頁,若發現FileIsAccept值為True,就代表著上傳檔案已被接受,即刻透過RegisterStartupScript函式以Javascript將隱藏的IFRAME顯示出來(當使用者按下UploadHandler.ASPx中的Button按紐時,這裡會用Javascript將IFRAME隱藏起來)。此頁面要放在主頁面的IFRAME中,下面是主頁面的程式碼。
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>
<iframe id="fileframe" src=UploadHandler.ASPx frameborder="0"
scrolling="no" ></iframe>
<ASP:UpdatePanel ID="UpdatePanel1" UpdateMode="Conditional" runat="server">
<ContentTemplate>
<ASP:Timer ID="Timer1" runat="server" Interval="100" ontick="Timer1_Tick">
</ASP:Timer>
</ContentTemplate>
</ASP:UpdatePanel>
</div>
</form>
</body>
</Html>
Default.ASPx.cs
using System;
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;
using System.Reflection;
public partial class _Default : System.Web.UI.Page
{
protected void Timer1_Tick(object sender, EventArgs e)
{
if (Session["FileIsToLarge"] != null) //上傳檔案過大,Redirect.
{
Session.Remove("FileIsToLarge");
Response.Redirect("MaxRequestLength.htm");
}
else if (Session["FileIsAccept"] != null)
//接受上傳,將IFrame重新設為可視. {
Session.Remove("FileIsAccept");
ScriptManager.RegisterStartupScript(this, GetType(),
"DisplayFrame", "$get(''fileframe'').style.visibility = ''visible'';", true);
}
}
}
另外,當上傳超過限制時,此例會導向另一網頁來顯示錯誤,下面是該網頁的Html程式碼。
MaxRequestLength.htm
<!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>
<title>Untitled Page</title>
</head>
<body>
Upload File size to large.
</body>
</Html>
結果如我們所預期的,當選了一個較大(大於maxRequestLength設定值)的檔案來上傳時,此例立刻會導向MaxRequestLength.htm來顯示錯誤。下面是web.config的組態設定,其實大多是ASP.Net AJax預設的設定,我標示出可改變上傳限制的部份,讀者們可自行修改。
web.config
<?XML version="1.0"?>
<configuration>
<configSections>
<sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication"/>
<sectionGroup name="webServices" type="System.Web.Configuration.ScriptingWebServicesSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<section name="jsonSerialization" type="System.Web.Configuration.ScriptingJSonSerializationSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="Everywhere"/>
<section name="authenticationService" type="System.Web.Configuration.ScriptingAuthenticationServiceSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication"/>
</sectionGroup>
</sectionGroup>
</sectionGroup>
</configSections>
<system.web>
<pages>
<controls>
<add tagPrefix="ASP" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</controls>
</pages>
<!--
Set compilation debug="true" to insert debugging
symbols into the compiled page. Because this
affects performance, set this value to true only
during development.
-->
<compilation debug="true">
<assemblIEs>
<add assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</assemblIEs>
</compilation>
<httpHandlers>
<remove verb="*" path="*.asmx"/>
<add verb="*" path="*.asmx" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add verb="*" path="*_APPService.axd" validafalse" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="false"/>
</httpHandlers>
<httpModules>
<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</httpModules>
<!-- 可修改maxRequestLength的值來改變上傳限制,單位為K,預設是4 MB -->
<httpRuntime maxRequestLength="4096"/>
</system.web>
<system.web.extensions>
<scripting>
<webServices>
<!-- Uncomment this line to customize maxJSonLength and add a custom converter -->
<!--
<jsonSerialization maxJSonLength="500">
<converters>
<add name="ConvertMe" type="Acme.SubAcme.ConvertMeTypeConverter"/>
</converters>
</JSonSerialization>
-->
<!-- Uncomment this line to enable the authentication service. Include requireSSL="true" if appropriate. -->
<!--
<authenticationService enabled="true" requireSSL = "true|false"/>
-->
<!-- Uncomment these lines to enable the profile service. To allow profile properties to be retrIEved
and modifIEd in ASP.Net AJax applications, you need to add each property name to the readAccessPropertIEs and
writeAccessPropertIEs attributes. -->
<!--
<profileService enabled="true"
readAccessPropertIEs="propertyname1,propertyname2"
writeAccessPropertIEs="propertyname1,propertyname2" />
-->
</webServices>
<!--
<scriptResourceHandler enableCompression="true" enableCaching="true" />
-->
</scripting>
</system.web.extensions>
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules>
<add name="ScriptModule" preCondition="integratedMode" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</modules>
<handlers>
<remove name="WebServiceHandlerFactory-Integrated"/>
<add name="ScriptHandlerFactory" verb="*" path="*.asmx" preCondition="integratedMode" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add name="ScriptHandlerFactoryAppServices" verb="*" path="*_APPService.axd" preCondition="integratedMode" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add name="ScriptResource" preCondition="integratedMode" verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</handlers>
</system.webServer>
</configuration>
圖2、3是執行結果。
圖2
當上傳檔案超過上限時的畫面如圖3。
圖3
二部曲
在下一篇文章中,我將與各位分享如何撰寫上傳進度回報的功能,當然!網路上已經有很多這種例子,但我使用了另一種技巧,不是ActiveX、HttpModule、Flash或是Silverlight,而是單純的ASP.Net AJax手法,執行結果如圖4,下次再見了。
圖4
上傳限制之範例下載:
http://www.dreams.idv.tw/~code6421/files/