0

I am trying to put a web interface on a lengthy server side process which should send regular progress\statistic reports to the client as the process is running. How can I do this?

Here is what I have attempted so far. The session in the webmethod is null for as long as the loop is processing. Once the loop is finished and you press the start button again, it is able to pick up the session value and populate the label. How do I get this to send updates to the client while the process is running?

I am using VS2012 and ASP.NET 4.5.

EDIT: To be more specific, the problem occurs while the server is busy with the loop. If I take the loop away and simply try to pull a variable value from the server at regular intervals then there is no problem. Put that variable in a loop and try and fetch it at regular intervals and you'll see what the problem is, the code I have posted should clarify the issue if you run it.

Thanks.

Default.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="ClientProgressTest.Default" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
    <script type="text/javascript">
        function GetCount() {

        $.ajax({
            type: "POST",
            url: "Default.aspx/GetCount",
            contentType: "application/json; charset=utf-8",
            data: {},
            dataType: "json",
            success: function (data) {
                lblCount.innerHTML = data.d;
            },
            error: function (result) {
                alert(result.status + ' ' + result.statusText);
            }
        });

        setTimeout(GetCount, 5000);
    }
</script>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <label id="lblCount"></label>
        <br />
        <br />
        <asp:Button ID="btnGetCount" runat="server" Text="Get Count" OnClientClick="GetCount();" OnClick="btnGetCount_Click" />
    </div>
    </form>
</body>
</html>

Default.aspx.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.UI;
using System.Web.UI.WebControls;

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

        }

        [WebMethod(EnableSession = true)]
        public static string GetCount()
        {
            string count = null;

            if (HttpContext.Current.Session["count"] != null)
            {
                count = HttpContext.Current.Session["count"].ToString();
            }

            return count;
        }

        protected void btnGetCount_Click(object sender, EventArgs e)
        {
            Session["count"] = null;

            for (int i = 0; i < 10000000; i++)
            {
                Session["count"] = i;
            }
        }
}
}
user1662409
  • 157
  • 3
  • 12
  • 1
    If you're not too concerned about support for older browsers, this sounds like a good place to use [web api](http://www.asp.net/web-api) and [web sockets](http://en.wikipedia.org/wiki/WebSocket). – RichardTowers Dec 05 '12 at 13:59
  • Consider to use the `complete:` from the ajax call to start your timer. Now your timer can expire before the ajax call has completed, causing a new ajax call to start before the previous one has completed. – bart s Dec 05 '12 at 14:00

4 Answers4

1

Typical web application frameworks are not well suited for this kind of thing. That is to say this isn't just a drawback of ASP.Net you'll run into this in PHP, RoR and I suspect many other frameworks in the future.

Long running tasks should be pawned off on processes that exist outside of IIS. There's no reason to bog down your web server with something that isn't about serving web pages. You can do this by either spawning a process from within your web application code or create a service that is constantly running in the background and checking some common resource (like a database) for jobs.

In either case the common resource would serve as a progress indicator. The external process would continuously update some value say, PercentComplete as the task moves along. Meanwhile your web application would need some kind of method for checking this value and returning it to the client. An AJAX web method would probably give you the best results.

Spencer Ruport
  • 34,215
  • 11
  • 81
  • 141
  • "You can do this by either spawning a process from within your web application code" - Can you give an example or more details please? – user1662409 Dec 05 '12 at 16:03
  • This question should give you a good start: http://stackoverflow.com/questions/6817777/execute-a-command-line-utility-in-asp-net – Spencer Ruport Dec 05 '12 at 16:37
  • "create a service that is constantly running in the background and checking some common resource (like a database) for jobs". This along with Darren's example is what I'm after. Thanks. – user1662409 Dec 05 '12 at 16:59
1

Create a web service that will return the current progress for the process. Set up a timer in the client script that when fired, gets the current progress from the web service then dynamically updates the client-side display element with the new value.

Kevin
  • 664
  • 3
  • 4
1

I put a lengthy answer to this question here : How to update a status label inside ajax request

For the sake of others being able to find the same solution i'll paste the answer here too:

==================================================================================

This example is using JQuery for the AJAX and the code-behind is in vb.net.

Essentially what you need to do is make your first call to begin the long process, then make a second call, repeatedly, using a second method to get the status of the long one.

AJAX

This is your main call to the long process. If needed, you will need to pass in the data to the method. Notice there is a processName. This should be a random string of sorts to ensure that you get the status of this process only. Other users will have a different random processName so you don't get confused status.

JAVASCRIPT

    var processName = function GenerateProcessName() {

        var str = "";
        var alhpabet = "abcdefghijklmnopqrstuvwxyz";
        for (i = 1; i < 20; i++) {
            str += alhpabet.charAt(Math.floor(Math.random() * alhpabet.length + 1));
        }
        return str;
    }


    function StartMainProcess(){
    $j.ajax({
            type: "POST",
            url: "/MyWebservice.asmx/MyMainWorker",
            data: "{'processName' : '" + processName + "','someOtherData':'fooBar'}",
            contentType: "application/json; charset=utf-8",
            dataType: "json",
            success: function (msg) {
                if (msg.d) {                        
                    // do a final timerPoll - process checker will clear everything else
                    TimerPoll();
                }
            }
        });
        TimerPoll();
     }

Your second AJAX call will be to another method to get the progress. This will be called every XXX time by a timer method.

This is the TimerPoll function; which will fire every 3 seconds in this case

JAVASCRIPT

function TimerPoll() {
        timer = setTimeout("GetProgress()", 3000)
    }

And finally, the GetProgress() function to, well, get the progress. We have to pass in the same processName used above, to get the process of this users call only

JAVASCRIPT

function GetProgress() {
        // make the ajax call
        $j.ajax({
            type: "POST",
            url: "/MyWebService.asmx/MyMainWorkerProgress",
            data: "{'processName' : '" + processName + "'}",
            contentType: "application/json; charset=utf-8",
            dataType: "json",
            success: function (msg) {

                // Evaulate result..
                var process = msg.d

                if (process.processComplete) {
                    // destroy the timer to stop polling for progress
                    clearTimeout(timer);

            // Do your final message to the user here.                      

                } else {

                   // show the messages you have to the user.
                   // these will be in msg.d.messages

                    // poll timer for another trip
                    TimerPoll();
                }
        });

    }

Now, in the back-end, you will have a couple of web methods that your AJAX communicates with. You will also need a shared/static object to hold all of the progress information, along with anything you want to pass back to the user.

In my case, i created a class which has its properties filled and passed back with every call to MyMainWorkerProcess. This looks a little like this.

VB.NET

    Public Class ProcessData
        Public Property processComplete As Boolean
        Public Property messages As List(Of String) = New List(Of String)
    End Class

I also have a shared property using this class, which looks like... ( this shared property is capable of holding multiple process progresses by multiple users - hence the dictionary. the key of the dictionary will be the process name. All progress data will be in the class ProcessData

Private Shared processProgress As Dictionary(Of String, ProcessData) = New Dictionary(Of String, ProcessData)

My main worker function looks a little like this. Notice that we first make sure there isn't another processProgress with the same

VB.NET

<WebMethod()> _
<ScriptMethod(ResponseFormat:=ResponseFormat.Json)> _
Public Function MyMainWorker(ByVal processName as string, ByVal SomeOtherData as string) as Boolean

        '' Create progress object - GUI outputs process to user
        '' If the same process name already exists - destroy it
        If (FileMaker.processProgress.ContainsKey(processName)) Then
            FileMaker.processProgress.Remove(processName)
        End If

        '' now create a new process
        dim processD as ProcessData = new ProcessData() with {.processComplete = false}


        '' Start doing your long process.

        '' While it's running and after whatever steps you choose you can add messages into the processData which will be output to the user when they call for the updates
         processD.messages.Add("I just started the process")

         processD.messages.Add("I just did step 1 of 20")

         processD.messages.Add("I just did step 2 of 20 etc etc")

         '' Once done, return true so that the AJAX call to this method knows we're done..
        return true

End Function

now all that is left is to call the progress method..All this is going to do is return the dictionary processData that has the same processName we set up earlier..

VB.NET

<WebMethod()> _
    <ScriptMethod(ResponseFormat:=ResponseFormat.Json)> _
    Public Function MyMainWorkerProgress(ByVal processName As String) As ProcessData

        Dim serializer As New JavaScriptSerializer()

        If (FileMaker.processProgress.ContainsKey(processName)) Then
            Return processProgress.Item(processName)
        Else
            Return New ProcessData()
        End If

    End Function

And voila..

So to recap..

  1. Create 2 web methods - one to do the long process and one to return it's progress
  2. Create 2 separate calls to these web methods. The first is to the main worker, the second, which is repeated xx seconds, to one that will give the progress
  3. Output your messages to the user however you see fit...

don't shoot me if there are a few typos :)

Community
  • 1
  • 1
Darren Wainwright
  • 28,457
  • 19
  • 68
  • 116
  • Your technique is basically the same except that you are calling two web methods. I've made these adjustments to match your code above and unfortunately it did not work. the label simply doesn't update. – user1662409 Dec 05 '12 at 15:55
  • Not really. you're performing a PostBack on the same button that's trying to count the long process. So you won't actually "See" the count changing. In mine I have 2 methods. One to start the process (count in your case) via ajax. the second method is like your GetCount. It's the second method you use to output to the client. None of mine perform any kind of postback. You need to separate them into 2 distinct processes, one for running and one for checking. You can't do both with one. – Darren Wainwright Dec 05 '12 at 16:01
  • The code I posted is part of code I wrote for a production environment that gets used every day. Works well :) – Darren Wainwright Dec 05 '12 at 16:02
  • I think the difference is that my server side process is in a loop, you don't specify this in your example but I'm guessing you're doing a step by step process and not a loop? – user1662409 Dec 05 '12 at 16:07
  • you also don't need to use sessions for this either. As in my example the data sent back is first added to a dictionary that has an ID set by the client. This way multiple clients can be running the same code without any data clashing. Using Sessions for this has the potential to really slow things down with multiple users. – Darren Wainwright Dec 05 '12 at 16:07
  • Doesn't matter what your server side is, loop or - in my case, creating PDF files. The point is you need 1 ajax call to start your loop and another to check its progress. without sounding harsh, the main difference is that mine works, and yours won't. – Darren Wainwright Dec 05 '12 at 16:08
  • Yes I agree with you on sessions. I have changed everything to match your code and it doesn't work. Can you clarify whether your server process is a loop? – user1662409 Dec 05 '12 at 16:09
  • I promise you, it works. And yes, in this case my long process is performing a loop. In real-time it's a process that can take anywhere from a few seconds to several minutes. – Darren Wainwright Dec 05 '12 at 16:10
  • Erm, it does matter because it works when you don't use a loop and doesn't work when you are using a loop. Maybe I wasn't clear but I'm specifically looking for a solution of fetching updates from the server while the server is busy looping. As my demo code clearly shows. – user1662409 Dec 05 '12 at 16:11
  • It doesn't matter.... you only think it works when not using a loop and that's because it performs your method so fast without the loop. – Darren Wainwright Dec 05 '12 at 16:12
  • I can't help anymore than I have already. I use this code in production daily. it's vastly different from yours. though that said, you have the starting blocks in place. The long process can be anything you want. just keep in mind 1) No postbacks *EVER* 2) one ajax call to start the process 3) 2nd ajax call to get the progress. 4) when your long one is running populate a shared static property (dictionary in my case) and when checking the process you read from the same property. – Darren Wainwright Dec 05 '12 at 16:15
  • Thanks darren, I appreciate you trying to help out. To clarify the problem is definitely specific to the loop. If I take it away and simply try and fetch a value from the server then there is no issue with yours or my code. Thanks for your input. – user1662409 Dec 05 '12 at 16:17
  • No, it's not. You're not understanding. The problem is that you are performing a postback. Your code 1st runs the javascript and THEN posts back. it doesn't do them async. This is why when you remove the loop it looks faster. You CANNOT do this with postbacks. My code does them *At the same time*. When mine runs I get to see a textbox populating with all the information i'm setting in my dictionary *as it happens* – Darren Wainwright Dec 05 '12 at 16:18
  • "The problem is that you are performing a postback". Ok so basically the server side code needs to be in a separate web service. That makes sense and concurs what others have suggested. Many thanks for your help. – user1662409 Dec 05 '12 at 16:53
  • Precisely. Just extract all the code above and re-factor it to your needs, it works :) and you're welcome. – Darren Wainwright Dec 05 '12 at 16:55
0

Probably the most up to date and fun way of implementing (if you're not hiving it off outside the web server process) this would be to use Signal R hubs to relay back messages to the client. Check it out on git hub here https://github.com/SignalR/SignalR

Luke Baughan
  • 4,458
  • 2
  • 28
  • 51