在傳統桌面項目中,進度條隨處可見,但作為一個很好的用戶體驗,卻沒有在如今主流的B/S程序中得到傳承,不能不說是個遺憾。這個遺憾並非WEB程序不支持進度條,很大的原因應該是我們由於種種麻煩懶的去實現。前段時間由於新項目等待客戶驗收,有點閑暇時間,於是突發奇想決定給新系統的所有導出功能添加進度提示,替換現正使用的只簡單顯示一個Loading圖片作為提示的方法,於是有了本文。
實現的思路很簡單:服務器端收到客戶端一個需要較長時間執行的任務 -> 服務器端開始執行這個任務並創建一個 Progress 狀態,保存在 Cache 中,在任務執行過程中,不斷更新進度 -> 同時,客戶端新取一個線程(異步),每隔0.5秒(經過測試,0.5秒是一個比較好的時間,既不容易造成客戶端網絡擁堵,也能帶來相當好的用戶體驗)訪問一次服務器以獲得該任務的進度並呈現給終端用戶。
下面是本人的實現方法,它能支持所有需要精確計算進度的任務,覺得有一定的參考性,前后台代碼分別采用 Javascript 和 C# 實現,分享給大家。
服務器端我們需要做 2 件事情:進度管理類和一個供客戶端查詢狀態的頁面(采用 Handler實現)。
首先是進度管理類,主要用於記錄任務總數和當前已經完成的數目,同時自行管理緩存狀態,以方便客戶端隨時訪問。代碼如下:
using Cache;
using System;
namespace RapidWebTemplate.WebForm {
/// <summary>
/// 服務器事件進度服務
/// </summary>
public sealed class ProgressService {
// 緩存保存的時間,可根據自己的項目設置
private const int CACHE_SECONDS = 600;
// 任務唯一ID
private string _key = null;
private ProgressService() { }
/// <summary>
/// 獲取或設置總進度
/// </summary>
public int Total { get; set; }
/// <summary>
/// 獲取或設置已經完成的進度
/// </summary>
public int Elapsed { get; set; }
/// <summary>
/// 獲取已經完成的進度百分比
/// </summary>
public byte ElapsedPercent {
get {
if (Finished) { return 100; }
double d = (double)Elapsed / (double)Total * 100d;
var tmp = Convert.ToInt32(Math.Ceiling(d));
if (tmp > 100) { tmp = 100; }
if (tmp < 0) { tmp = 0; }
return Convert.ToByte(tmp);
}
}
/// <summary>
/// 獲取一個值,該值指示當前進度是否已經完成
/// </summary>
public bool Finished {
get { return Elapsed >= Total; }
}
public void Remove() {
try {
CacheFactory.Remove(_key);
} catch { }
}
/// <summary>
/// 獲取一個緩存中的進度對象或創建一個全新的進度對象並添加到緩存中
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public static ProgressService GetInstance(string key) {
var obj = CacheFactory.GetCache(key) as ProgressService;
if (obj == null) {
obj = new ProgressService();
obj._key = key;
CacheFactory.Add(key, obj, DateTime.Now.AddSeconds(CACHE_SECONDS));
}
return obj;
}
}
}
接下來是查詢頁面,命名為 Progress.ashx,后台代碼如下:
using RapidWebTemplate.WebForm;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Utility;
using Utility.Http;
namespace Web.Handlers {
/// <summary>
/// 獲取服務端指定任務執行的進度
/// </summary>
public class Progress : IHttpHandler {
public void ProcessRequest(HttpContext context) {
context.Response.ContentType = "text/json";
var key = FormHelper.GetString(context.Request.QueryString["progress_key"]);
var obj = ProgressService.GetInstance(key);
context.Response.Write(obj.ToJSON());
if (obj.Finished) {
obj.Remove();
}
}
public bool IsReusable {
get { return false; }
}
}
}
到此,我們已經完成了后台代碼的編寫,下面將是前台代碼的編寫,這里以導出 Excel 為例,代碼如下:
var js={
doExport:function(icon){
var key=''+(new Date().getTime())+(Math.floor(Math.random()*10));
var btns=$('#btnExport');
var showProgress=false;
if(btns.size()>0){
//var form=btns.first().parent('form');
//form.attr('target','_blank');
var input=$('#download_key');
if(input.size()>0){
input.val(key);
}else{
btns.first().parents('form').append('<input type="hidden" id="download_key" name="download_key" value="'+key+'"/>');
}
btns.first().trigger('click');
showProgress=true;
}else{
js.info('Not supported.');
}
var me=this;
setTimeout(function(){
$(document.body).hideLoading();
if(showProgress){
me._showProgress(key);
}
},500);
},
_showProgress:function(key){
var id='progress_bar';
var me=this;
if($('#'+id).size()>0){
}else{
$(document.body).append('<div id="'+id+'_dialog"><div id="'+id+'"></div></div>');
}
$('#'+id+'_dialog').dialog({
//title:'\u8bf7\u7a0d\u540e...', // please wait
width:400,
//height:60,
modal:true,
closable:false,
border:false,
noheader:true,
onOpen:function(){
$(this).children().first().progressbar({value:0});
setTimeout(function(){
me._updateProgessState(key,id,me);
},1000);
}
});
},
_progressStateTimer:null,
_updateProgessState:function(key,id,ns){
var url='/Handlers/Progress.ashx?progress_key='+key;
url+='&ran='+(new Date().getTime());
$.get(url,function(res){
//res={"Total":0,"Elapsed":0,"ElapsedPercent":100,"Finished":true}
if(res.Finished){
$('#'+id).progressbar('setValue',100);
setTimeout(function(){ $('#'+id+'_dialog').dialog('destroy');},500); // Wait for 0.5 seconds to close the progress dialog
clearTimeout(ns._progressStateTimer);
}else{
//alert(res.Elapsed);
$('#'+id).progressbar('setValue',res.ElapsedPercent);
ns._progressStateTimer=setTimeout(function(){ns._updateProgessState(key,id,ns);},500);
}
});
},
};
所有必要的代碼已經編寫完成,下面是服務器端對進度服務的使用,還是以導出 Excel 為例,需要在原有的代碼基礎上,加入進度的管理(注釋部分的A、B、C 3 段),代碼如下:
/// <summary>
/// 導出當前列表數據到Excel
/// </summary>
protected void ExportToExcel() {
using (var outputStm = new MemoryStream()) {
using (var document = new SLDocument()) {
var fields = this.ExportedFields;
//先輸出表頭
for (var i = 0; i < fields.Count; i++) {
document.SetCellValue(1, i + 1, fields[i].Label);
}
//輸出內容
if (GridControl != null) {
var ps = ProgressService.GetInstance(FormHelper.GetString(Request.Form["download_key"])); // A:創建進度
var f = GetFilter();
var itemCount = 0;
var source = GetGridSource(f, GridControl.GetSortExpression().ToOrder(CurrentDAL), int.MaxValue, 1, out itemCount);
ps.Total = itemCount; // B: 設置總進度
var row = 2;
object value = null;
foreach (var item in source) {
for (var col = 0; col < fields.Count; col++) {
#if DEBUG
System.Threading.Thread.Sleep(50);
#endif
value = item.GetType().GetProperty(fields[col].Field).GetValue(item, null);
document.SetCellValue(row, col + 1, value);
}
ps.Elapsed += 1; // C: 更新已經完成的進度
row++;
}
}
document.SaveAs(outputStm);
}
outputStm.Position = 0;
WebUtility.WriteFile(outputStm, outputStm.Length, this.Response, ExportedFileName + ".xlsx");
}
}
最后附上效果圖

