摘要:
文件上传是日常开过程中最常用的功能之一,目前实现文件上传的方式多种多样。这其中较为复杂的情况就是关于大文件、多文件上传的问题,目前解决大文件、多文件上传一般借助于js或者flash组件,今天就同大家一起看一下如何使用silverlight实现这个功能,而且功能和用户体验相对会更好一些。
主要内容:
一、组件特点
二、实现原理
三、编码实现
一、组件特点
对于今天要说的组件姑且叫做"CmjUpload"吧,方便称呼。目前有很多上传组件来辅助完成日常开发,"CmjUpload"有什么特点呢:
- 解决大文件、多文件上传问题
- 基于asp.net上传,不需要部署WCF、WebService操作方便
- 接口丰富、灵活性强,配置使用方便。
- 支持选择、拖拽两种文件添加方式上传,用户体验好。
- 支持取消、暂停、继续操作满足续传要求。
OK,就说那么多吧,主要是让大家有兴趣看下去,其实之所以有今天的话题主要还是为了学习以及满足实际开发需求。
二、实现原理
在Silverlight中要实现上传有很多方式,例如说使用WCF或者WebService,但是考虑到实际情况,这里没有选择以上两种方式,而是选择了WebRequest方式。原因比较简单,部署十分方便,不需要为了上传组件而进行额外的配置。Silverlight中使用WebRequest同其他.Net开发中的使用方式是类似的,不同的是Silverlight中很多操作都是异步的,当然WebRequest也不例外。此外,在这里需要对一个文件分块发送,一方面可以解决大文件上传问题,另一方面可以实时显示文件上传进度。下面一个简单的交互过程:
当然要完成整个组件远不止上面说的这些,UI的设计,组件的本地化,用户接口的设计等都是必须思考的问题。下面是组件界面原型:
界面分为两个区域:文件显示区域和操作区域,当然这里的文件区域本身也是可以操作的,例如如果你不想点击按钮选择文件的话,可以选择直接拖拽一个或多个文件到文件区域。还可以对已添加的文件进行删除操作,对正在上传的文件进行暂停和续传操作。此外文件区域的设计主要提供文件信息显示,例如缩略图、上传进度、文件名称、文件大小等信息。操作区域一方面提供文件整体信息的显示(例如文件总数、已上传数等),另一方面提供了文件浏览、上传、清空操作。
下面是类的设计:
在上图中我们可以看出有三个包:Core、Config、Util。
Core是核心包,里面主要包括文件队列管理(FileQueue)、文件上传控制(FileUpload)、文件界面区域(FileArea)、文件大小单位转换(FileSize)、缩略图控制(FileIcon)。
Config是配置和接口包,主要包括组件设计级别常量(注意不是用户级别也不是开发级别,开发级别配置在接口中进行)(UploadConstant)、客户端开发接口(ExposeInterface)、本地化实现(Localization)、接口注册(ClientInteraction)。
Util包主要包括一些常用辅助类,主要包括xml操作(XmlHelper)、服务器端文件保存辅助类(CmjUpload)。
三、编码实现
有了上面的分析相信下面的实现就相当容易理解了,首先看一下文件上传类FileUpload:
using
System;
using
System.Net;
using
System.Windows;
using
System.Windows.Controls;
using
System.Windows.Documents;
using
System.Windows.Ink;
using
System.Windows.Input;
using
System.Windows.Media;
using
System.Windows.Media.Animation;
using
System.Windows.Shapes;
using
System.Text;
using
System.IO;
using
System.Windows.Threading;
using
CmjUpload.Util;
using
CmjUpload.Config;
namespace
CmjUpload
{
public
class
FileUpload
{
//开始上传
public
delegate
void
StartUploadHanler(
object
sender,EventArgs e);
public
event
StartUploadHanler StartUpload;
public
void
OnStartUpload(
object
sender, EventArgs e)
{
if
(StartUpload !=
null
)
{
StartUpload(sender, e);
}
}
// 上传
public
delegate
void
UploadingHanler(
object
sender, ProgressArgs e);
public
event
UploadingHanler Uploading;
public
void
OnUploading(
object
sender, ProgressArgs e)
{
if
(Uploading !=
null
)
{
Uploading(sender,e);
}
}
//上传结束
public
delegate
void
UploadCompletedHanler(
object
sender, EventArgs e);
public
event
UploadCompletedHanler UploadCompleted;
public
void
OnUploadCompleted(
object
sender, EventArgs e)
{
if
(UploadCompleted !=
null
)
{
UploadCompleted(sender, e);
}
}
private
string
_requestUrl =
""
;
private
string
_fileName =
""
;
private
long
_fileLength = 0;
private
long
_blockLength = 4096;
//单次上传文件大小
private
long
_postedLength = 0;
//已传输文件大小
private
long
_nextLength = 0;
//下次传输的文件大小
private
bool
_firstUpload =
true
;
private
BinaryReader _fileReader =
null
;
private
UploadStatus _uploadStatus = UploadStatus.Start;
public
FileInfo File
{
get
;
set
;
}
//public long PostedLength
//{
// get
// {
// return _postedLength;
// }
// set
// {
// _postedLength = value;
// }
//}
public
UploadStatus Status
{
get
{
return
_uploadStatus;
}
set
{
_uploadStatus = value;
}
}
public
void
Upload(FileInfo file)
{
this
.File = file;
//XmlHelper xmlHelper = new XmlHelper("Config/CmjUploadConfig.xml");
//_requestUrl=xmlHelper.GetAttibuteValue("Upload", "RequestUrl");
_requestUrl = ExposeInterface.Instance().RequestUrl;
this
._fileName =
this
.File.Name;
this
._fileLength =
this
.File.Length;
this
._blockLength = FileSize.GetLockSize(
this
._fileLength);
//this._postedLength = 0;
_fileReader =
new
BinaryReader(file.OpenRead());
//_uploadStatus = UploadStatus.Start;
if
(_fileLength < _blockLength)
{
_nextLength = _fileLength;
}
else
{
_nextLength = _blockLength;
}
OnStartUpload(
this
,
new
EventArgs());
UploadInBlock();
}
public
void
UploadInBlock()
//上传一块数据
{
UriBuilder uriBuilder =
new
UriBuilder(
new
Uri(_requestUrl, UriKind.Absolute));
uriBuilder.Query =
string
.Format(
"fileName={0}&status="
+_uploadStatus,
this
._fileName);
WebRequest request = WebRequest.Create(uriBuilder.Uri);
request.Method =
"POST"
;
request.ContentType =
"multipart/mixed"
;
//注意这里
request.ContentLength = _nextLength;
if
(_firstUpload)
{
_uploadStatus = UploadStatus.Uploading;
_firstUpload =
false
;
}
request.BeginGetRequestStream((IAsyncResult asyncResult) =>
{
WebRequest rqst = asyncResult.AsyncState
as
WebRequest;
Stream rqstStm = rqst.EndGetRequestStream(asyncResult);
byte
[] buffer =
new
byte
[_blockLength];
int
size = _fileReader.Read(buffer, 0, buffer.Length);
if
(size>0)
{
rqstStm.Write(buffer, 0, size);
rqstStm.Flush();
_postedLength += size;
if
((_fileLength - _postedLength) < _blockLength)
{
_nextLength = _fileLength-_postedLength;
}
}
rqstStm.Close();
rqst.BeginGetResponse((IAsyncResult ascResult) =>
//开始数据传输
{
OnUploading(
this
,
new
ProgressArgs() { Percent = ((
double
)_postedLength / (
double
)_fileLength) });
WebRequest webRequest = ascResult.AsyncState
as
WebRequest;
WebResponse webResponse = (WebResponse)webRequest.EndGetResponse(ascResult);
StreamReader reader =
new
StreamReader(webResponse.GetResponseStream());
string
responsestring = reader.ReadToEnd();
reader.Close();
if
(_postedLength >= _fileLength)
{
_uploadStatus = UploadStatus.Complelte;
}
if
(_uploadStatus == UploadStatus.Uploading)
{
UploadInBlock();
}
//else if(_uploadStatus==UploadStatus.Cancel)
//{
// return;
//}
else
if
(_uploadStatus==UploadStatus.Complelte)
{
_fileReader.Close();
OnUploadCompleted(
this
,
new
EventArgs());
}
}, request);
}, request);
}
/// <summary>
/// 继续上传
/// </summary>
/// <param name="fileName"></param>
/// <param name="uploadedLength"></param>
//public static void ContinueUplaod(string fileName,long uploadedLength)
//{
//}
}
//上传进度参数
public
class
ProgressArgs:EventArgs
{
public
double
Percent
{
get
;
set
;
}
}
public
enum
UploadStatus
{
Start,
Uploading,
Cancel,
Complelte
}
}
|
在这个类中需要注意的是状态的控制,因为组件需要实现文件暂停、续传功能,并且每次请求时需要发送相应的操作状态;另一点就是对外公开了三个事件,用于给UI提供进度支持和状态通知。
FileQueue管理整个文件队列,控制着界面UI、文件上传等信息:
using
System;
using
System.Collections.Generic;
using
System.IO;
namespace
CmjUpload
{
/// <summary>
/// 文件队列管理者
/// </summary>
public
class
FileQueue
{
private
static
object
_lock =
new
object
();
private
static
FileQueue _fileQueue =
null
;
private
Dictionary<
string
,
int
> _fileIndexs =
null
;
//文件同索引对应关系
private
Dictionary<
string
,FileInfo> _files =
null
;
private
Dictionary<
string
,FileArea> _fileAeas =
null
;
private
Dictionary<
string
, FileUpload> _fileUploader =
null
;
private
int
index = 0;
private
FileQueue()
{
_fileIndexs =
new
Dictionary<
string
,
int
>();
_files =
new
Dictionary<
string
, FileInfo>();
_fileAeas =
new
Dictionary<
string
, FileArea>();
_fileUploader =
new
Dictionary<
string
, FileUpload>();
}
public
static
FileQueue Instance()
{
lock
(_lock)
{
if
(_fileQueue ==
null
)
{
_fileQueue =
new
FileQueue();
}
}
return
_fileQueue;
}
public
void
Add(FileInfo file)
{
_fileIndexs.Add(file.Name, index);
_files.Add(file.Name,file);
FileArea fileAerea =
new
FileArea(file);
_fileAeas.Add(file.Name, fileAerea);
++index;
}
public
void
Remove(
string
fileName)
{
_fileIndexs.Remove(fileName);
_files.Remove(fileName);
_fileAeas.Remove(fileName);
_fileUploader.Remove(fileName);
}
public
Dictionary<
string
,FileInfo> Files
{
get
{
return
_files;
}
set
{
_files = value;
}
}
public
Dictionary<
string
, FileArea> FileAreas
{
get
{
return
_fileAeas;
}
set
{
_fileAeas = value;
}
}
public
Dictionary<
string
, FileUpload> FileUploader
{
get
{
return
_fileUploader;
}
set
{
_fileUploader = value;
}
}
public
int
GetFileIndex(
string
fileName)
{
int
i=-1;
if
(_fileIndexs.ContainsKey(fileName))
{
i = _fileIndexs[fileName];
}
return
i;
}
public
void
Clear()
{
string
[] tempFileNames=
new
string
[
this
.Files.Count];
this
.Files.Keys.CopyTo(tempFileNames,0);
foreach
(
string
fileName
in
tempFileNames)
{
this
.Remove(fileName);
}
}
}
}
|
FileArea用于构建每个文件的UI展示:
using
System;
using
System.Net;
using
System.Windows;
using
System.Windows.Controls;
using
System.Windows.Documents;
using
System.Windows.Ink;
using
System.Windows.Input;
using
System.Windows.Media;
using
System.Windows.Media.Animation;
using
System.Windows.Shapes;
using
System.Windows.Media.Imaging;
using
System.IO;
using
Cmj.MyWeb.MySilverlight.MyUserControl.Button;
namespace
CmjUpload
{
public
class
FileArea
{
private
FileInfo _file =
null
;
//private int _number = 0;
private
Grid _container =
null
;
private
TextBlock _name =
null
;
private
Image _thumnail =
null
;
private
ProgressBar _progress =
null
;
private
TextBlock _size =
null
;
private
TextBlock _percent =
null
;
private
Cancel _cancel =
null
;
private
Pause _pause =
null
;
private
Play _continue =
null
;
private
Check _complete =
null
;
public
FileArea(FileInfo file)
{
_file = file;
//_number = number;
_container =
new
Grid();
_container.Name =
"fileArea_container_"
+ file.Name;
_container.ColumnDefinitions.Add(
new
ColumnDefinition() { Width =
new
GridLength(60)});
_container.ColumnDefinitions.Add(
new
ColumnDefinition());
_container.ColumnDefinitions.Add(
new
ColumnDefinition() { Width=
new
GridLength(60)});
_container.ColumnDefinitions.Add(
new
ColumnDefinition() { Width =
new
GridLength(60) });
_container.Height = 50;
_thumnail =
new
Image();
_thumnail.Name =
"fileArea_thumnail_"
+ file.Name;
_thumnail.Height = 40;
_thumnail.Source = FileIcon.Instance().GetThumbnailImage(file);
_thumnail.VerticalAlignment = VerticalAlignment.Bottom;
_thumnail.HorizontalAlignment = HorizontalAlignment.Center;
Grid.SetColumn(_thumnail, 0);
_progress =
new
ProgressBar();
_progress.Name =
"fileArea_progress_"
+ file.Name;
_progress.Minimum = 0;
_progress.Maximum = 100;
_progress.Value = 0;
_progress.Height = 20;
_progress.VerticalAlignment = VerticalAlignment.Bottom;
//_progress.HorizontalAlignment = HorizontalAlignment.Center;
Grid.SetColumn(_progress, 1);
_name =
new
TextBlock();
_name.Name =
"fileArea_name_"
+ file.Name;
_name.Text = file.Name;
_name.VerticalAlignment = VerticalAlignment.Bottom;
_name.HorizontalAlignment = HorizontalAlignment.Left;
_name.Margin =
new
Thickness(10, 0, 0, 2);
Grid.SetColumn(_name, 1);
_percent =
new
TextBlock();
_percent.Name =
"fileArea_percent_"
+ file.Name;
_percent.VerticalAlignment = VerticalAlignment.Bottom;
_percent.HorizontalAlignment = HorizontalAlignment.Right;
_percent.Margin =
new
Thickness(0, 0, 10, 2);
Grid.SetColumn(_percent, 1);
_size =
new
TextBlock();
_size.Name =
"fileArea_size_"
+ file.Name;
_size.VerticalAlignment = VerticalAlignment.Bottom;
_size.HorizontalAlignment = HorizontalAlignment.Right;
Grid.SetColumn(_size, 2);
_cancel =
new
Cancel();
_cancel.Name =
"fileArea_cancel_"
+file.Name;
_cancel.Width = 15;
_cancel.Height = 15;
_cancel.VerticalAlignment = VerticalAlignment.Bottom;
//_cancel.Click += new RoutedEventHandler(_cancel_Click);
Grid.SetColumn(_cancel, 3);
_pause =
new
Pause();
_pause.Name =
"fileArea_pause_"
+ file.Name;
_pause.Width = 15;
_pause.Height = 15;
_pause.VerticalAlignment = VerticalAlignment.Bottom;
_pause.Visibility = Visibility.Collapsed;
Grid.SetColumn(_pause, 3);
_continue =
new
Play();
_continue.Name =
"fileArea_continue_"
+ file.Name;
_continue.Width = 15;
_continue.Height = 15;
_continue.VerticalAlignment = VerticalAlignment.Bottom;
_continue.Visibility = Visibility.Collapsed;
Grid.SetColumn(_continue, 3);
_complete =
new
Check();
_complete.Name =
"fileArea_complete_"
+ file.Name;
_complete.Width = 18;
_complete.Height = 18;
_complete.VerticalAlignment = VerticalAlignment.Bottom;
_complete.Visibility = Visibility.Collapsed;
Grid.SetColumn(_complete, 3);
_container.Children.Add(_thumnail);
_container.Children.Add(_progress);
_container.Children.Add(_size);
_container.Children.Add(_name);
_container.Children.Add(_percent);
_container.Children.Add(_cancel);
_container.Children.Add(_pause);
_container.Children.Add(_continue);
_container.Children.Add(_complete);
}
public
Grid Container
{
get
{
return
_container;
}
set
{
_container = value;
}
}
public
TextBlock Name
{
get
{
return
_name;
}
set
{
_name = value;
}
}
public
Image Thumnail
{
get
{
return
_thumnail;
}
set
{
_thumnail = value;
}
}
public
ProgressBar Progress
{
get
{
return
_progress;
}
set
{
_progress = value;
}
}
public
TextBlock Size
{
get
{
return
_size;
}
set
{
_size = value;
}
}
public
TextBlock Percent
{
get
{
return
_percent;
}
set
{
_percent = value;
}
}
public
Cancel Cancel
{
get
{
return
_cancel;
}
set
{
_cancel = value;
}
}
public
Pause Pause
{
get
{
return
_pause;
}
set
{
_pause = value;
}
}
public
Play Continue
{
get
{
return
_continue;
}
set
{
_continue = value;
}
}
public
Check Complete
{
get
{
return
_complete;
}
set
{
_complete = value;
}
}
}
}
|
ExposeInterface用于向客户端调用提供操作接口:
using
System;
using
System.Net;
using
System.Windows;
using
System.Windows.Controls;
using
System.Windows.Documents;
using
System.Windows.Ink;
using
System.Windows.Input;
using
System.Windows.Media;
using
System.Windows.Media.Animation;
using
System.Windows.Shapes;
using
System.Collections.Generic;
using
System.Windows.Browser;
namespace
CmjUpload.Config
{
public
class
ExposeInterface
{
private
static
object
_lock =
new
object
();
private
static
ExposeInterface _exposeInterface =
null
;
private
string
_fileTypes =
string
.Empty;
private
string
_fileDialogFilter =
string
.Empty;
private
long
_limitSize = 0;
private
int
_limitCount = 0;
private
ExposeInterface()
{
}
public
static
ExposeInterface Instance()
{
lock
(_lock)
{
if
(_exposeInterface ==
null
)
{
_exposeInterface =
new
ExposeInterface();
}
}
return
_exposeInterface;
}
[ScriptableMember]
public
string
FileTypes
//ex:*.jpg|*.gif
{
get
{
return
_fileTypes;
}
set
{
_fileTypes = value;
}
}
[ScriptableMember]
public
string
FileDialogFilter
{
get
{
if
(
this
._fileDialogFilter ==
string
.Empty&&
this
._fileTypes!=
string
.Empty)
{
string
[] types =
this
._fileTypes.Split(
'|'
);
string
[] filters=
new
string
[types.Length];
for
(
int
i=0;i<types.Length;++i)
{
filters[i] =
"("
+types[i] +
")|"
+ types[i];
}
_fileDialogFilter =
string
.Join(
"|"
,filters);
}
return
_fileDialogFilter;
}
set
{
_fileDialogFilter = value;
}
}
[ScriptableMember]
public
long
LimitSize
//单位 MB
{
get
{
return
_limitSize;
}
set
{
_limitSize = value;
}
}
[ScriptableMember]
public
int
LimitCount
{
get
{
return
_limitCount;
}
set
{
_limitCount = value;
}
}
[ScriptableMember]
public
string
RequestUrl
{
get
;
set
;
}
public
List<
string
> GetFileExtensions()
{
List<
string
> extensions =
new
List<
string
>();
string
[] types =
this
._fileTypes.Split(
new
char
[] {
'|'
}, StringSplitOptions.RemoveEmptyEntries);
foreach
(
string
type
in
types)
{
extensions.Add(type.TrimStart(
'*'
));
}
return
extensions;
}
}
}
|
CmjUpload用于提供给服务器端进行文件操作,服务端只需要简单调用其Save方法就可以进行文件保存:
using
System;
using
System.Collections.Generic;
using
System.Web;
using
System.IO;
namespace
CmjUpload.Web.Util
{
public
class
CmjUpload
{
public
static
void
Save(
string
relationPath)
{
string
fileName = HttpContext.Current.Request[
"fileName"
];
Save(relationPath,fileName);
}
public
static
void
Save(
string
relationPath,
string
outputName)
{
string
status = HttpContext.Current.Request[
"status"
];
if
(status ==
"Start"
)
{
using
(FileStream fs = File.Create(Path.Combine(relationPath, outputName)))
{
SaveFile(HttpContext.Current.Request.InputStream, fs);
}
}
else
if
(status ==
"Uploading"
)
{
using
(FileStream fs = File.Open(Path.Combine(relationPath, outputName), FileMode.Append))
{
SaveFile(HttpContext.Current.Request.InputStream, fs);
}
}
else
if
(status ==
"Completed"
)
{
HttpContext.Current.Response.Write(
"{success:true}"
);
}
}
private
static
void
SaveFile(Stream stream, FileStream fs)
{
byte
[] buffer =
new
byte
[4096];
int
bytesRead;
while
((bytesRead = stream.Read(buffer, 0, buffer.Length)) != 0)
{
fs.Write(buffer, 0, bytesRead);
}
}
}
}
|
OK,其他的代码就不再贴出了,看一下客户端如何使用吧。
为了方便使用客户端提供一个公用js类CmjUpload.js:
//注意要在控件加载完之后调用,建议放到插件onload事件中初始化(<param name="onLoad" value="pluginLoaded" />)
var
CmjUpload =
function
(options) {
var
uploader =
null
;
if
(options.hasOwnProperty(
"id"
)) {
//组件id
uploader = document.getElementById(options.id);
}
else
{
alert(
"Please configure the id attribute before use CmjUpload component!"
);
return
;
}
if
(options.hasOwnProperty(
"requestUrl"
)) {
//请求的url
uploader.content.cmjUpload.RequestUrl = options.requestUrl;
}
else
{
alert(
"Please configure the requestUrl attribute before use CmjUpload component!"
);
return
;
}
if
(options.hasOwnProperty(
"fileTypes"
)) {
//文件类型限制
uploader.content.cmjUpload.FileTypes = options.fileTypes;
}
if
(options.hasOwnProperty(
"limitCount"
)) {
//每批次上传的文件数
uploader.content.cmjUpload.LimitCount = options.limitCount;
}
if
(options.hasOwnProperty(
"limitSize"
)) {
//单个文件大小限制
uploader.content.cmjUpload.LimitSize = options.limitSize;
}
}
CmjUpload.prototype.onBeforeFileUpload =
function
() {
//单个文件上传之前执行
}
CmjUpload.prototype.onFileUploading =
function
() {
//单个文件上传时执行
}
CmjUpload.prototype.onFileUploaded =
function
() {
//单个文件上传完毕执行
}
CmjUpload.prototype.onBatchUploaded =
function
() {
//整个批次的文件上传完毕执行
}
|
然后在页面添加上传组件(本地化语言在param中进行配置):
<
object
id="cmjUpload1" data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
<
param
name="source" value="ClientBin/CmjUpload.xap"/>
<
param
name="onError" value="onSilverlightError" />
<
param
name="onLoad" value="pluginLoaded" />
<!--注意这里,必须保证配置信息在页面加载之后执行-->
<
param
name="culture" value="en-US" />
<!--注意这里本地化配置-->
<
param
name="uiculture" value="en-US" />
<!--注意这里本地化配置-->
<
param
name="background" value="white" />
<
param
name="minRuntimeVersion" value="4.0.50826.0" />
<
param
name="autoUpgrade" value="true" />
<
a
href="http://go.microsoft.com/fwlink/?LinkID=149156&v=4.0.50826.0" style="text-decoration:none">
<
img
src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight" style="border-style:none"/>
</
a
>
</
object
>
|
使用时在页面引用该类,进行id和url等信息配置,具体的配置内容上面js已经注释的很清楚,这里不再赘余。例如对页面做如下配置:
pluginLoaded=
function
(){
var
upload =
new
CmjUpload({ id:
'cmjUpload1'
, requestUrl:
'http://localhost:3407/Upload.aspx'
, fileTypes:
'*.jpg|*.png|*.wmv|*.rar|*.iso'
, limitCount: 5, limitSize: 150 });
}
|
后台文件执行文件保存操作:
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Web;
using
System.Web.UI;
using
System.Web.UI.WebControls;
using
System.IO;
namespace
CmjUpload.Web
{
public
partial
class
Upload : System.Web.UI.Page
{
protected
void
Page_Load(
object
sender, EventArgs e)
{
CmjUpload.Web.Util.CmjUpload.Save(
"f:\\"
);
}
}
}
|
下面是使用效果:
类型限制
大小限制
数量限制
删除一个文件
上传中
上传暂停
完成上传
手动清空(即使不手动清空继续上传文件会自动清空,清空操作主要用于上传暂停后不需要上传的清空)
下面看看本地化设置为英文后的效果
我们通过修改LimitCount来看一下大文件传输,好几个G的文件同时传输也没有问题
OK,最后附上组件下载,使用方法上面说的也比较清楚了。关于组件源代码就不再提供下载了,相信读完这篇文章要实现并不难,真正需要的话可以给我留言或发邮件KenshinCui@hotmail.com。