在传统桌面项目中,进度条随处可见,但作为一个很好的用户体验,却没有在如今主流的B/S程序中得到传承,不能不说是个遗憾。这个遗憾并非WEB程序不支持进度条,很大的原因应该是我们由于种种麻烦懒的去实现。前段时间由于新项目等待客户验收,有点闲暇时间,于是突发奇想决定给新系统的所有导出功能添加进度提示,替换现正使用的只简单显示一个Loading图片作为提示的方法,于是有了本文。

实现的思路很简单:服务器端收到客户端一个需要较长时间执行的任务 -> 服务器端开始执行这个任务并创建一个 Progress 状态,保存在 Cache 中,在任务执行过程中,不断更新进度 -> 同时,客户端新取一个线程(异步),每隔0.5秒(经过测试,0.5秒是一个比较好的时间,既不容易造成客户端网络拥堵,也能带来相当好的用户体验)访问一次服务器以获得该任务的进度并呈现给终端用户。

下面是本人的实现方法,它能支持所有需要精确计算进度的任务,觉得有一定的参考性,前后台代码分别采用 Javascript 和 C# 实现,分享给大家。

服务器端我们需要做 2 件事情:进度管理类和一个供客户端查询状态的页面(采用 Handler实现)。

首先是进度管理类,主要用于记录任务总数和当前已经完成的数目,同时自行管理缓存状态,以方便客户端随时访问。代码如下:


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72

​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,后台代码如下:


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

​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 为例,代码如下:


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68

​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 段),代码如下:


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

​​​​​/// <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"​​​​);​​​​​​​​}​​​​​​​​}​


最后附上效果图

居于Web的进度条实现思路(下载百分比)_缓存

 ​