在一个长时间运行的.aspx(C#)页面中实时显示后台执行过程和进度(jQuery Ajax $.post)

本人是刚毕业进入公司的菜鸟,关于asp.net和C#方面也完全是新手,写这个是作为开发笔记,也希望给遇到类似问题的菜鸟参考,当然欢迎任何人批评指正。。。

#你需要一些基础知识才能流畅的阅读本文,本人在写这篇文章以及其描述的开发的时候,已经初步掌握了javascript、html、css、C#、web form开发,另外还使用了Ajax技术,此外还需要对jQuery和C#多线程概念有基本的了解

不想看我啰嗦,想直接看示例代码的请直接滚动至“脚本代码”或者文章底部的完整示例项目下载链接
抱歉,没找到怎么实现页内跳转

一、前言

前一段时间被安排了一个任务,需要在原来的页面中加入一个上传本地Excel表,并将关键的几列上传到数据库的功能。。。。。。
一开始是很顺利的写出了大部分代码,小问题在网上也很容易找到解决方案,直到我写完功能代码之后,因为Excel表可能会非常庞大,导致后台处理过程很耗时,这时候前台的web页面是处于挂起的状态,看起来就像卡了一样,所以需要将后台过程实时提示到页面上,对于这样的功能,丝毫没有头绪
我尝试在隐藏代码中,需要进行步骤提示的地方加入Response.Write("");语句,然而结果是所有的显示都是在隐藏代码执行完毕后,最后一起显示到页面上
在google上查到的原因是,线程问题,默认的.aspx页面中所有的代码都是在单线程中运行的,因此直到隐藏代码完成执行之后才会刷新页面

貌似可以用out.flush();分块逐步显示刷新页面,应该是把原本的一个单线程分成了连续的多个单线程,和多线程的区别类似于硬件电路中的串行和并行的区别,个人理解,请指正
这种办法貌似不能解决我的问题,反正我是不知道怎么搞
参考链接:高性能WEB开发(11) - flush让页面分块,逐步呈现

问题是我对C#和asp.net的多线程开发技术也不懂啊,,,我TM是新手,你上来就让我开发多线程??
事情一定不会这么狗血,而且,我只是想显示个实时提示而已,难道还要从零开始学多线程。。。。。。
直到我无意中看到了Ajax技术。。。然后有种,果然显示个实时提示是不需要从零开始学习多线程的,哈哈哈哈哈

补个示例项目的目录结构
在一个长时间运行的.aspx(C#)页面中实时显示后台执行过程和进度(jQuery Ajax $.post)_第1张图片

二、Ajax - post()方法

#下面仅介绍本文用到的内容,想要深入了解请自行搜索“jQuery post”、“ajax post”、“$.get $.post”等关键字

post()方法通过HTTP POST请求以异步的方式从服务器载入数据
jQuery中的$.post()方法貌似是$.ajax()方法的高层实现,我的理解就是进一步封装,简化了使用过程,当然也会少一些功能,比如没有请求失败后的执行函数。。。palapala。。。OK,$.post()方法的背景了解到此结束

本文使用的是这个:$.post(url, data, success[callback]);
url,就是使用post方法请求的服务器地址
data,在请求的同时发送给服务器的数据,以key/value的形式,在服务器url的隐藏代码中,用Request.Form["key"]接收
success[callback],服务器请求成功后的回调函数,服务器url在成功运行结束后会紧接着执行回调函数
文章底部放了完整的项目下载链接,需要的请直接滚动至文章底部

脚本代码
//script.js
function AjaxPostRequest(threadStatus, filePath)
{
    //$.post(url, data, success[callback]);
    $.post(
        "AjaxPostServer.aspx",    //Post参数,服务请求地址

        {    //Post参数,发向服务的参数,以key/value形式
            status: threadStatus, //status=0,表示线程尚未创建,status=1表示线程已创建
            file: filePath //需要在后台任务中进行处理的Excel文件的路径
        },
        
        function (data) {    //Post参数,成功后的回调函数

            var backJson = eval('(' + data + ')'); //将json字符串解析为json对象

            //页面提示
            $("#UploadLog2").html("后台任务执行到位置:" + backJson.position + "
" + backJson.massage); //判断,当后台任务还没有运行到结尾时,调用递归方法进行实时状态查询 if (backJson.position != "5") { AjaxPostRequest("1", filePath); } } ); }

为了让脚本代码部分更加清晰,所以把另外两种提示代码单独放在了下面,我尝试向阅读这篇文章的同学展示更多种页面提示的方式,包括进度条等等,也就是显示实时提示和提示方式是无关的,我总共写了三种提示方式,在文章底部的下载链接里包含的项目示例中包含了全部的代码

            //---------------------进度条提示---------------------
            $("#UploadPrograss").css({
                "width": "500px",
                "height": "16px",
                "border": "1px solid gray"
            });
            $("#Prograss").css({
                "height": "16px",
                "left": "0px",
                "top": "0px",
                "background": "#46dd5a"
            });
            $("#Prograss").css({ "width": String(20 * parseInt(backJson.position)) + "%" })
            //---------------------进度条提示---------------------

            //---------------------增量式进度提示---------------------
            if ("0" == backJson.position) {
                $("#UploadLog").html("正在读取本地Excel文件...");
            }
            else if ("1" == backJson.position) {
                $("#UploadLog").html("本地Excel文件读取完成。
正在解析本地Excel文件..."); } else if ("2" == backJson.position) { $("#UploadLog").html("本地Excel文件读取完成。
本地Excel文件解析完成。
正在验证本地Excel文件..."); } else if ("3" == backJson.position) { $("#UploadLog").html("本地Excel文件读取完成。
本地Excel文件解析完成。
本地Excel文件验证完成。
正在转换本地Excel文件..."); } else if ("4" == backJson.position) { $("#UploadLog").html("本地Excel文件读取完成。
本地Excel文件解析完成。
本地Excel文件验证完成。
本地Excel文件转换完成。
正在上传本地Excel文件..."); } else if ("5" == backJson.position) { $("#UploadLog").html("本地Excel文件读取完成。
本地Excel文件解析完成。
本地Excel文件验证完成。
本地Excel文件转换完成。
本地Excel文件上传完成。
"); } //---------------------增量式进度提示---------------------

下面是三种提示方式的简单效果,好吧,确实很简陋,但是这只是作为演示来用还是够的,哈哈哈


在一个长时间运行的.aspx(C#)页面中实时显示后台执行过程和进度(jQuery Ajax $.post)_第2张图片

以下是写这部分内容时参考的链接
http://www.w3school.com.cn/jquery/ajax_post.asp
https://api.jquery.com/jQuery.post/
jQuery $.post $.ajax用法
浅谈HTTP中Get与Post的区别

三、页面和隐藏代码

文章底部放了完整的项目下载链接,需要的请直接滚动至文章底部

页面代码
//default.aspx


    
    
    


    
请选择以.xls和.xlsx为后缀的Excel文件




隐藏代码
//default.aspx.cs
using System;
using System.IO;

namespace RealTimeTips
{
    public partial class _default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        protected void Upload_Click(object sender, EventArgs e)
        {
            // 检查FileUpload1控件是否上传正确格式的文件
            string fileExt = Path.GetExtension(FileUpload1.FileName).ToLower();
            if (".xlsx" != fileExt && ".xls" != fileExt)
            {
                FileExtTips.Visible = true;
                return;
            }
            Directory.CreateDirectory(Server.MapPath("~/ExcelTemp/"));
            string filePath = Server.MapPath("~/ExcelTemp/") + FileUpload1.FileName;
            FileUpload1.SaveAs(filePath);
            FileExtTips.Visible = false;

            //调用脚本创建新的线程用以执行后续任务
            filePath = filePath.Replace(@"\", @"\\");//传送到js的字符串会被二次转义,因此需要在传送前进行处理
            ClientScript.RegisterStartupScript(ClientScript.GetType(), "", "");
        }
    }
}

四、服务器URL代码

这部分是实现多线程的主体,当然主要后台任务也是放在这部分完成的

服务器页面AjaxPostServer.aspx被设计为两部分,一部分使用多线程技术用来实现异步线程,也就是新建立一个线程并将ThreadTask()函数放到新建立的线程中运行
这样只要把我们想在异步线程中完成的任务放到ThreadTask()函数中就行了
另外一部分则是在主线程中调用ThreadResponse()函数,用于查询Session["log"]的内容,Session["log"]则是用来实时传递线程间的消息
这样只要在一开始调用线程创建的部分,之后不停的递归调用服务器页面的ThreadResponse()函数就可以实时显示后台进度了

AjaxPostServer.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="AjaxPostServer.aspx.cs" Inherits="AjaxPostServer"%>
AjaxPostServer.aspx.cs
using System;
using System.IO;
using System.Threading;

public partial class AjaxPostServer : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if ("0" == Request.Form["status"])
        {
            //线程未创建,调用初始化函数
            InitialThread();
        }
        else if ("1" == Request.Form["status"])
        {
            //线程已存在,进行状态查询
            if (null != Session["log"] && "" != Session["log"].ToString())
            {
                Response.Write(Session["log"].ToString());
            }
        }
    }

    private void InitialThread()
    {
        Session["log"] = ""; //初始化Session["log"],用于传递线程间消息
        Thread newThread = new Thread(new ParameterizedThreadStart(ThreadTask)); //带参数的线程实例化
        newThread.Start(Request.Form["file"]); //启动线程并传入Excel文件路径
        Response.Write("{'position':'0','massage':''}"); //初始化json字符串
    }

    //用于完成主要的后台任务
    private void ThreadTask(object filePath)
    {
        //对filePath路径下的Excel文件进行本地处理,可以上传数据到数据库,将想要实时显示到页码上的数据放入到Session["log"]中,递归调用的JS函数会实时将结果刷新到页面上
        Session["log"] = "{'position':'0','massage':'这里用于显示消息提示或者错误详情'}";
        Thread.Sleep(1000); //延时1s,便于观察状态变化
        Session["log"] = "{'position':'1','massage':'这里用于显示消息提示或者错误详情'}";
        Thread.Sleep(1000); //延时1s,便于观察状态变化
        Session["log"] = "{'position':'2','massage':'这里用于显示消息提示或者错误详情'}";
        Thread.Sleep(1000); //延时1s,便于观察状态变化
        Session["log"] = "{'position':'3','massage':'这里用于显示消息提示或者错误详情'}";
        Thread.Sleep(1000); //延时1s,便于观察状态变化
        Session["log"] = "{'position':'4','massage':'这里用于显示消息提示或者错误详情'}";
        Thread.Sleep(1000); //延时1s,便于观察状态变化
        Session["log"] = "{'position':'5','massage':'后台任务执行完成!'}";
        File.Delete(filePath.ToString()); //任务完成后删除保存在服务器的Excel文件
    }
}

事实上,Session["log"]的变化能够被实时显示到页面上,是因为Session["log"]变化是在另一个线程中发生的,而这个时候主线程则是在不停的递归调用进行状态查询并将结果刷新到页面上,,,从而实现了实时的状态显示效果,这就是为什么要把主要任务放在另一个线程去做的原因,用主线程来刷新页面是很自然的做法

但是有个问题,想要在后台异步运行的主要工作内容和页面之间传递信息,就要先穿过主页面到达服务器页面,然后从服务器页面的主线程中到达服务器页面创建的新线程中,这样信息才能被主要任务接收到

所以我在考虑能不能直接在主页面的隐藏代码中开启一个异步线程用于完成后台任务,这样如果我想传递信息和数据只需要从隐藏代码的主线程到达异步线程就行了
好吧,至少听上去简洁了很多,哈哈哈

我在尝试实现这个功能的时候,最早是因为看到了下面这个帖子,但是后来发现帖子中的实现方法并不能完全满足自己的要求,所以就推翻自己重写了,虽然说是重写,但是在$.post方法的使用和思路上还是差不多的。。。。。。不过人家的帖子写的简洁清晰,不像我这么啰嗦,在这里感谢以及推荐,哈哈哈哈
ASP.NET 多线程 监控任务执行情况,并显示进度条

RealTimeTips.zip项目下载
链接: https://pan.baidu.com/s/1o8Dl0pG 密码: dg49

写在最后

这样的实现会造成大量信息传递时的操作相对繁琐,比如我想从服务器页面的异步线程中传回大量的数据显示到页面中,需要用回调函数传回json字符串,然后用js解析再进行页面更新。。。还有我最开始的做法是直接对excel文件流进行处理和解析的,但是为了将文件放在异步线程中去处理,只好把文件转存在服务器本地,把保存的地址传送到异步线程中,然后再读取和解析。。。。。。等等
当然如果服务器的异步线程中只需要进行类似于上传数据到数据库之类的操作,则不需要考虑大量信息传递的事情了。。。
我现在尝试直接在主页面的隐藏代码中使用异步线程来显示实时页面提示,我不知道能不能做出来,如果可以的话,那应该会有另外一篇在一个长时间运行的.aspx(C#)页面中实时显示后台执行过程和进度(异步线程)这样的文章。。。。。。还请路过的大神指教
如果做不到,那我也进一步学习了C#多线程的技术,哈哈哈哈

//以下为2017年9月30号更新

最后的尝试失败了(我在尝试解决问题的过程中发现自己的思考方向可能是错误的,然后我就放弃了,我觉得这样的尝试并没有太多意义)
我意识到自己可能犯了一个常识性的错误

我在上面最后写到,希望能绕过JS直接在C#隐藏代码中实现实时的页面后台进度提示
但是问题就在于,我一开始就没完全搞懂,整个实现的机制
具体地说就是实时页面提示并不完全是线程问题,还有服务器和客户端的问题
JS代码是在本地的客户端(浏览器)中运行的,而C#隐藏代码则是在服务器中运行的
这就导致了,如果我想实时的刷新本地的浏览器页面,那么使用JS来完成最后的页面刷新工作才是最自然的做法

真实情况就是我根本就不知道如何在C#隐藏代码中实时刷新页面,而在C#隐藏代码中使用JS脚本注入的方式来实现,显然是一件本末倒置的事(而且我也没能成功)
我已经在C#隐藏代码中实现了多线程,线程间的参数传递也相对更加简单了,但是我却没办法在C#隐藏代码中把线程反馈的结果实时的刷新到页面上

然后我才明白,这件事本来就不是这个样子的,所谓的常识性错误则是指:试图绕过JS代码,用在服务器上运行的C#代码去刷新本地的客户端页面。。。。。。
好吧,现在问题清楚了,实时页面刷新,使用JS来实现最后的刷新步骤本来就是最自然的做法,上面用$.post(url, data, success[callback]);,就是很自然很好的实现办法

反思

显而易见的是,我一开始没能确切的弄清楚这件事,ASP.NET开发和我原来所做过的Swift、C++开发都不一样,那就是并不是所有的代码都是在一个地方运行的,一部分运行在本地的客户端(浏览器),另一部分则是在服务器上运行的
我想作为一个稍有经验的ASP.NET开发者都是很清楚的知道这个的;作为新手来说,我确实在一开始就知道这个,然而真正去开发的时候,我却潜意识的忽略掉了这个问题,潜意识仍然认为所有的代码都在一个机器上运行
我不清楚我说的这些是否正确,如果有错误,还请路过的同学指出,谢谢

你可能感兴趣的:(在一个长时间运行的.aspx(C#)页面中实时显示后台执行过程和进度(jQuery Ajax $.post))