使用XmlHttpRequest对象实现文件上传进度条
用过ajax的朋友应该有听过Asp.Net XmlHttpRequest对象,ajax其实就是通过XmlHttpRequest对象来向服务器发出异步请求,并从服务器获得数据,然后用javascript来操作DOM而更新页面。
本篇就是要通过XmlHttpRequest对象来实现实时的文件上传进度条显示。
效果图:
正文部分:
看过有些前辈的做法是通过设置HTTP请求的Refresh头字段来定时刷新页面从而显示进度,但是这样就会带动整个页面一起刷新,就算我们把进度条做成单独的页面,效果仍旧不是太好。我之前试过用ajax的Timer组件,但是不知道是何原因,Timer控件在IIS下预览时总是无法正常发挥作用。苦恼了好一阵子,怀疑是MS的BUG。最后发现了一个很好的替代办法就是利用XmlHttpRequest对象来自己实现定时刷新,这样每次只需向服务器请求很少的数据,减少了对服务器的压力,在后期的测试中,发现这个办法确实很好用,而且在IIS下也一切正常(上图就是IIS下运行的效果)。
当然如果光有进度条没有数据,那这个进度条也只能是个摆设,所以我把接下来的内容分成两块:进度信息的保存、进度的显示
1、进度信息的保存
首先我们要明白进度条在这里反应的是什么的进度?毫无疑问是文件上传的进度,我们对上传的文件数据进行了提取,也就是说这个提取的进度就是我们要显示给客户端的进度,这样才是文件上传进度条的意义。那就简单了,我们只要把已经提取的文件大小与总的文件大小比对一下,就可以知道完成的百分比了。可是问题来了,我们如何知道上传了多少了呢?答案肯定是要用一个变量来保存已经上传的数据量。那这个变量要放在哪里才能让我们既可以在进度页面中访问,又可以在HTTP上传模块中访问呢?
大家肯定知道一般情况下,用户在多个页面之间访问,会用到Session对象或URL传值来进行页面之前的通信。但是前一篇所介绍的HTTP模块并不属于一个页面,因此我们无法简单的应用Session让进度页面与上传模块实现通信。这里主要还是借鉴高山来客的思路:首先构建一个用于存放文件信息的类,该类主要用来保存文件信息,如:文件名,路径,当前上传的数据量,上传时间等。然后设置一个针对某次上传的唯一ID做为页面中通信的暗号,拥有这个暗号的页面才能获取对应于某次上传的文件信息。现在已经有了两个变量了,接着就要使这两个变量可以被多个页面所使用,方法就是在上传页面中,将这个ID变量注册为该页面的一个隐藏域,这样包含这个页面的HTTP请求流中就会包含那个上传ID。另一个类变量就保存在页面缓存Cache中,并用上传ID做为其编号。
现在假设已经有了这么一个用于存放文件信息的类UploadFileInfo。
首先我们要在上传页面的PageLoad中new一个ID,然后注册一个隐藏域用来保存此ID,同时实例化UploadFileInfo类,并将相应的信息写入该类,最后把该类放入Catch:
-
if (!IsPostBack)
-
{
-
UploadFileInfo ufi = new UploadFileInfo();
-
ufi.strFileGuid = Guid.NewGuid().
-
ToString;//用GUID来表示唯一的ID;
-
ufi.strTempDir = Server.MapPath
-
("TempUpload/" + ufi.strFileGuid + "//");
-
ClientScript.RegisterHiddenField
-
("UploadID", ufi.strFileGuid);
-
//隐藏域,名字为UploadID,值为ufi.strFileGuid
-
HttpContext.Current.Cache.Add(ufi.strFileGuid, ufi,
-
null, DateTime.Now.AddDays(10), TimeSpan.Zero,
-
System.Web.Caching.CacheItemPriority.High, null);//加入到Catch中
-
}
经过以上步骤,我们就可以在HTTP模块中访问了。
因为在这次的HTTP请求流中包含了一个隐藏域,所以我们可以对获取的HTTP请求流进行分析,从而获取相应的上传ID,也就是我们之前说的暗号。然后通过Cache的编号找到Cache中的文件信息对象,从而我们可以在后来的数据读取过程中对该对象的上传数据量进行修改。由于是放在Cache中,加之是一个引用对象,所以对该对象修改后,其它代码访问到的都是最新的值。
-
string sguid = GetUploadId(bPreloadedEnitityBody,
-
eContentEncode);//GetUploadId
-
是自己写的一个方法用来从请求流中获取上传ID
-
UploadFileInfo ufiFileInfo = (UploadFileInfo)HttpContext.
-
Current.Cache[sguid];//取出文件信息对象
其它页面如果要使用这个对象就得先获取ID,之后就可以自由操作了。
2、文件上传进度条的显示
从图中我们可以看到,当显示进度的时候,背后的页面成灰色,并且无法响应任何事件,有点类似模态窗口。这个效果大家可以在网上查查,还是挺容易实现的。我这里有一段js显示此效果的代码(搜集于网上):
-
functionModalDialog
-
(name,divid,width,height,leftop,topop,color)
-
{
-
this.name=name;//名称
-
this.div=divid;//要放入窗体中的元素名称
-
this.width=width;//窗体宽
-
this.height=height;//窗体高
-
this.leftop=leftop;//左侧位置
-
this.topop=topop;//上部位置
-
this.color=color;//整体颜色
-
this.show=function()//显示窗体
-
{
-
document.all(obj.name+"_divshow").style.
-
width=obj.width;
-
document.all(obj.name+"_divshow").style.
-
height=obj.height;
-
document.all(obj.name+"_divshow").style.
-
left=obj.leftop;
-
document.all(obj.name+"_divshow").style.
-
top=obj.topop;
-
document.all(obj.name+"_mask").style.
-
width=document.body.clientWidth;
-
document.all(obj.name+"_mask").style.
-
height=document.body.clientHeight;
-
document.all(obj.name+"_divshow").style.
-
visibility="visible";
-
document.all(obj.name+"_mask").style.
-
visibility="visible";
-
}
-
this.close=function()//关闭窗体
-
{
-
document.all(obj.name+"_divshow").style.width=0;
-
document.all(obj.name+"_divshow").style.height=0;
-
document.all(obj.name+"_divshow").style.left=0;
-
document.all(obj.name+"_divshow").style.top=0;
-
document.all(obj.name+"_mask").style.width=0;
-
document.all(obj.name+"_mask").style.height=0;
-
document.all(obj.name+"_divshow").
-
style.visibility="hidden";
-
document.all(obj.name+"_mask").
-
style.visibility="hidden";
-
}
-
this.toString=function()
-
{
-
vartmp="〈divid='"+this.name+"_divshow'
-
style='position:absolute;left:0;top:0;z-index:10;
-
visibility:hidden;width:0;height:0'〉";
-
tmp+="〈tablecellpadding=0cellspacing=
-
0border=0width=100%height=100%〉";
-
tmp+="〈tr〉";
-
tmp+="〈tdid='"+this.name+"_content'valign=top〉〈/td〉";
-
tmp+="〈/tr〉"
-
tmp+="〈/table〉";
-
tmp+="〈/div〉";
-
tmp+="〈divid='"+this.name+"_mask'
-
style='position:absolute;top:0;left:0;width:0;height:0;
-
background:#666;filter:ALPHA(opacity=50);
-
z-index:9;visibility:hidden'〉〈/div〉";
-
document.write(tmp);
-
document.all(this.name+"_content").insertBefore
-
(document.all(this.div));
-
}
-
varobj=this;
-
}
接着讲我们的重点:如何实现定时局部刷新。关于XmlHttpRequest对象,我这里就不详细讲述了,提供大家一个关于此的手册下载。为了大家更容易理解,我举个小例子:
-
//页面A.aspx
-
functionreturnresponse(url)
-
{
-
varxmlHttp=newActiveXObject('MSXML2.xmlHttp');
-
if(xmlHttp!=null)
-
{
-
xmlHttp.open("GET",url,true);
-
//向URL指定的页面发送GET请求
-
xmlHttp.onreadystatechange=function()
-
{//当xmlHttp的readyState
-
改变的时候就会引发这个事件
-
if(xmlHttp.readyState==4&&xmlHttp.status==200)
-
{//4="成功发送",200="所请求的页面返回正常"
-
temp=xmlHttp.responseText;
-
//接收所请求页面发回的数据
-
alert(temp);
-
}
-
}
-
xmlHttp.send(null);
-
}
-
else
-
{
-
alert("浏览器不支持XmlHttp.");
-
}
-
}
-
//URL所指向的页面B的代码.cs,
-
当然也可以是同一个页面的cs
-
if(Request.QueryString["event"]=="test")
-
{
-
Response.Write("测试");
-
}
-
-
/**//*
-
然后我们在A页中执行returnresponse
-
(B.aspx?event="test");
-
很快就会发现在A页中弹出一个窗口,内容是"测试"。
-
*/
通过以上小例子,大家应该已经对该XmlHttpRequest对象有所了解了吧。为实现定时刷新,我把进度条单独放在一个页面中(如A.aspx),通过js的setTimeout来定时执行类似returnresponse这样的方法,然后在A.aspx.cs代码中获取文件信息对象,接着通过Response来反馈进度信息。这样在A.aspx页面中就可以获取到信息,并进行显示了。但是执行ActiveXObject将要花费不少代价,而且我们是定时执行该方法,显然会造成性能下降。在参考了构建一个pool来管理无刷新页面的xmlhttp对象后,决定采用这一方法,事实证明该方法实现文件上传进度条确实有效。
-
functionxmlHttpPoolFactory()
-
{
-
this.XmlHttpPool=newArray();
-
this.MaxPoolLength=10;
-
this.Add=function()
-
{
-
if(this.XmlHttpPool.length〈this.MaxPoolLength)
-
{
-
xmlHttp=null;
-
if(window.XMLHttpRequest)
-
{//codeforallnewbrowsers
-
xmlHttp=newXMLHttpRequest();
-
}
-
elseif(window.ActiveXObject)
-
{//codeforIE5andIE6
-
try
-
{
-
xmlHttp=newActiveXObject('MSXML2.xmlHttp');
-
}
-
catch(e)
-
{
-
try
-
{
-
xmlHttp=newActiveXObject('Microsoft.xmlHttp');
-
}
-
catch(e2)
-
{
-
}
-
}
-
}
-
if(xmlHttp!=null)
-
{
-
this.XmlHttpPool.push(xmlHttp);
-
}
-
returnxmlHttp;
-
}
-
};
-
this.GetXmlHttp=function()
-
{
-
varxmlHttp=null;
-
varpool=this.XmlHttpPool;
-
for(vari=0;i〈pool.length;++i)
-
{
-
if(pool[i].readyState==4||pool[i].readyState==0)
-
{
-
xmlHttp=pool[i];
-
break;
-
}
-
}
-
if(xmlHttp==null)
-
{
-
returnthis.Add();
-
}
-
returnxmlHttp;
-
};
-
this.returnresponse=function(url,div)
-
{
-
varxmlHttp=this.GetXmlHttp();
-
varparam=div.split(',');
-
if(xmlHttp!=null)
-
{
-
xmlHttp.open("GET",url,true);
-
xmlHttp.onreadystatechange=function()
-
{
-
if(xmlHttp.readyState==4&&xmlHttp.status==200)
-
{//4="loaded",200="OK"
-
temp=xmlHttp.responseText;
-
vartemparray=temp.split(",");
-
document.getElementById(param[0]).
-
innerText=temparray[0];
-
document.getElementById(param[1]).
-
innerText=temparray[1]+"KB/S";
-
document.getElementById(param[2]).
-
innerHTML="
-
〈tablewidth='"+temparray[2]*3+"' 〉
-
〈tr 〉〈td 〉〈/td 〉〈/tr 〉〈/table 〉";
-
document.getElementById(param[3]).
-
innerText=temparray[2]+"%";
-
}
-
}
-
xmlHttp.send(null);
-
}
-
else
-
{
-
alert("YourbrowserdoesnotsupportxmlHttp.");
-
}
-
};
-
this.AportAll=function()
-
{
-
for(vari=0;i〈this.XmlHttpPool.length;++i)
-
{
-
this.XmlHttpPool[i].abort();
-
}
-
};
-
}
-
vara=newxmlHttpPoolFactory();//建立一个全局的工厂实例
-
varstrevent="";
-
functionrefresh(url,interval,div)
-
{//该方法我用来定时刷新,因为除了SetTimeout还有一些其它活要干
-
varstr1="";
-
for(i=2;i〈arguments.length;i++)
-
{//因为可能需要刷新的div不只一个,
-
所以利用js的arguments来解决动态参数的问题
-
if(i!=arguments.length-1)
-
{
-
str1=str1+arguments[i]+",";
-
}
-
else
-
{
-
str1=str1+arguments[i];
-
}
-
}
-
a.returnresponse(url,str1);//调用该方法实现异部通信
-
varstr="";
-
for(i=0;i〈arguments.length;i++)
-
{
-
if(i!=arguments.length-1)
-
str=str+"'"+arguments[i]+"',";
-
else
-
str=str+"'"+arguments[i]+"'";
-
}
-
setTimeout("refresh("+str+")",interval);//定时执行该方法
-
}