Executing a long-running process from a web page

SEPT 2014 UPDATE: See this follow-up post, which explains newer ways to do this, including using SignalR and making the UI code easier with AngularJS.


Well, this is now come up twice in a the last few months, so, per my personal policy I am writing a blog post on it!

DOWNLOAD: You can view the source or clone it locally via GitHub – click here

The issue is: what if you have a long-running process and want to kick it off from a web page. By default, you have 2 bad options:

  • Fire-and-forget – kick it off in a new thread and “hope for the best”! This is bad because something could go wrong and you wouldn’t know. Also, if your process runs more than 3 minutes and if there are no other people on the system, IIS is going to spin-down that application pool, cancelling the process.
  • Fire-and-wait – this is bad because it gives the user no information about what is going on (bad user experience). If the process is more than a couple of minutes, then you start running into timeouts too from IIS, to ASP.NET, to script timeout, etc

So, what would be an ideal way to handle this? For me, it would be to click a button, see some status and have it let me know when it’s done. Something like this:

image
image
image
image
image
image

So, how do you do something like this? Well, there are a few pieces to it:

  • jQuery on the client-side to kick off the process
  • jQuery on the client-side to check the status while running; and stop checking status when complete
  • A web service on the server-side for jQuery to communicate with – I used an .asmx web service
  • A proper class on the server-side that handles the long-running process and reports back progress

Let me try to explain how I approached this – I’ll start from the server-side and work my way out.

Server-Side Long-Running Class:
This is a class that kicks off the long-running process in a new Task. Then, in that other “thread” that process reports back status by updating public properties and firing events (depending on how the caller wants to be notified).

Here’s basically how that looks:
image

In a private method, where I simulate the long-running work, that simply just has some Thread.Sleep(..) statements to simulate latency. In your case, there is where your actual long-running processes would go.

image

You can look at the project to see the supporting methods (like UpdatePercent) – but hopefully on it’s own, this makes sense. We asynchronously kick off a long-running process in a Task – and in that task, it reports back status.

The Web Service:
The class above is server-side. We need a way to expose this information in a way that is consumable to the client-side, so we create a service. For AJAX functionality, I continue to use ASMX web services because A) they work and B) they are fast and easy.

This is pretty straight-forward, here is all the web service does. There is one method for kicking off the process and one method for getting the current status.

image

The Client-Side:
OK, so now we have a way to execute a long-running process on the server, and a web service that gives us access to it. Now it’s time to have the client-side actually do work, by interacting with the service.

I’m going to use jQuery and a jQueryUI progress bar, so I add this to my <head> section of the page:

<link rel="stylesheet" href="https://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
<script src="https://code.jquery.com/jquery-1.9.1.js"></script>
<script src="https://code.jquery.com/ui/1.10.3/jquery-ui.js"></script>

Now, I need to create some of the UI elements – like the button, the “div” of where I’ll show the status, etc.

<input type="button" id="startProcessButton" value="Start Process" />
<div id="ajaxImage" style="display: inline;"><img src="Images/AjaxLoading.gif" /></div>
<div id="statusDiv"></div>
<div id="progressbar" style="height: 20px; width: 200px;"></div>

You might note that I get all of my AJAX “spinner” graphics from this free site: http://www.ajaxload.info

OK, so lastly, we need some jQuery/JavaScript. We want to do is this:

  • Wait until the page/DOM is loaded
  • Run some code when that startProcessButton is clicked
  • Have that code turn off AJAX caching, call the web service to kick off the long-running process, and then monitor.
  • Monitoring will be done with “setInterval”. This is a JavaScript function that keeps running code every n milliseconds. If you capture the return value of setInterval, you can call clearInterval(handle) later to cancel this monitoring.
  • Monitoring will consist of contacting the web service every second to go get the status. Depending on what we find from the service, we will update the UI accordingly.

Here is how that code works:

// Wait until the DOM is loaded
$().ready(function () {

    // Go get handles to the UI elements that we need.
    var $ajaxImage = $("#ajaxImage");
    var $progressbar = $("#progressbar");
    var $statusDiv = $("#statusDiv");
    var $startProcessButton = $("#startProcessButton");

    // Hide the AJAX spinner image and the progress bar, by default.
    $ajaxImage.hide();
    $progressbar.hide();

    // Turn off AJAX caching. When we make an AJAX call, we always want the latest.
    $.ajaxSetup({ cache: false });

    // Simple error handling. If an AJAX error occurs, it wil show in our statusDiv.
    $(document).ajaxError(function (e, jqxhr, settings, exception) {
        $statusDiv.html("Error from AJAX call: " + jqxhr.statusText);
        $startProcessButton.removeAttr("disabled");
    });

    // Event for the when the startProcessButton is clicked
    $startProcessButton.click(function () {
        // Disable the button (it is re-enabled later)
        $startProcessButton.attr("disabled", "disabled");
        // Show "Loading..." for a status
        $statusDiv.html("Loading...");

        // Call the web service to kick off the process
        $.ajax({
            type: "GET",
            dataType: "json",
            url: "AjaxServices.asmx/StartProcess",
            contentType: "application/json; charset=utf-8"
        });

        // Set up a recurring timer to keep running this code every second
        // We use intervalHandle later via clearInterval to turn off this timer
        var intervalHandle = setInterval(function () {
            // Go hit the web service to get the latest status
            $.ajax({
                type: "GET",
                dataType: "json",
                url: "AjaxServices.asmx/GetStatus",
                contentType: "application/json; charset=utf-8",
                success: function (response, status, xhr) {
                    if (response.d == null) {
                        // The background process isn't running or there was a problem - reset.
                        $statusDiv.html("");

                        // Stop this setInterval loop that we're in.
                        clearInterval(intervalHandle);
                    }
                    else {
                        // We're running and have status - so:

                        // Show the AJAX spinner image and progress bar
                        $ajaxImage.show();
                        $progressbar.show();

                        // Set the status text and the progress back value
                        $statusDiv.html(response.d.PercentComplete + "% " + response.d.WorkDescription);
                        $progressbar.progressbar({
                            value: response.d.PercentComplete
                        });

                        // Process is complete, start to reset the UI (the response.d == null above, will do the rest).
                        if (response.d.IsComplete) {
                            $startProcessButton.removeAttr("disabled");
                            $ajaxImage.hide();
                            $progressbar.hide();
                        }
                    }
                }
            });

        }, 1000);
    });
});

So, that’s about it. Again, you can download a complete, self-contained demo version of this project with the link at the top of this post. Hopefully though, this explains how to have the client-side effectively communicate with the server-side to execute a long-running process.

Posted in ASP.NET, ASP.NET MVC, Best-practices, JQuery, Uncategorized, Web Services
10 comments on “Executing a long-running process from a web page
  1. Jason says:

    Where is the code for the UpdatePercent method in the server-side approach? I’m guessing it should be obvious since you didn’t include, but I am not able to figure it out.

    Like

    • Robert Seder says:

      It’s in the source code which is provided in the link at the top:

      private void UpdatePercent(Double newPercentComplete, String workDescription)
      {
      // Update the properties for any callers casually checking the status.
      this.PercentComplete = newPercentComplete;
      this.WorkDescription = workDescription;
      if (newPercentComplete == 100)
      IsComplete = true;

      // Fire the event for any callers who actively want to be notifed.
      if (ProgressChanged != null)
      ProgressChanged(this, new ProgressChangedEventArgs(
      newPercentComplete,
      IsComplete,
      workDescription
      ));
      }

      Like

      • Jason says:

        Okay, I’ve implemented your code, but ProgressChanged is always null. I am obviously missing something. I’ve never really fully learned how to deal with asynchronous calls. Usually, I can look something up and squeeze it in to get by, or develop a wrapper once and use it over and over, but have never became intimate with the internals. Or, I learn it and don’t use it enough to remember the next time I want to use it. Please share any code that is not posted in your article, if you don’t mind. The link does not work no matter where I click it from. When I don’t receive 403, I get that the file no longer exists.

        Like

      • Jason says:

        So, now I’ve wired up the ProgressChanged event to a method that updates a label to the percent, but the page is already fully posted back as the background process is running and even though the label text has changed, it does not reflect on that actual label.

        Like

      • Robert Seder says:

        Take a look at the full source code up on github:

        https://github.com/SamplesAndDemos/SederSoftware.Worker

        That might help. The key thing is that ASP.NET loads the page, but all other updating of the UI is done client-side with JavaScript. JavaScript reaches back to the web service to get data and then JavaScript/jQuery updates the elements on the page!

        Like

  2. […] Robert Seder on Executing a long-running process from a web page […]

    Like

  3. SG says:

    I am using jquery.signalR-1.1.4.js (.net Framework 4.0)

    and I am getting an error

    Error 10 ‘Owin.IAppBuilder’ does not contain a definition for ‘MapSignalR’ and no extension method ‘MapSignalR’ accepting a first argument of type ‘Owin.IAppBuilder’ could be found (are you missing a using directive or an assembly reference?)

    How do I fix it?

    Like

  4. Murtz says:

    You’ve used static WorkProcessor object. Wouldn’t that be shared across multiple web service calls? If so, what could be the alternative to it.

    Like

    • Robert Seder says:

      Yes, but it would stay alive for multiple requests. The trick here was to show the overall concept of how to handle multi-threading on the back-end, and something on the web front-end all in one “simple” project. In reality, it would be ideal to not do it this way at all. Ideally, you’d have a Windows Service which accepts requests and can report status back. That way, one stateful process would handle the heavy work AND that could be done on a middle-tier server without bogging down the web server.

      If you did want to keep this on the web server though, this could could be modified to use Session State to help share state information across multiple servers (which is yet another problem with this design). So again, this is meant to be an end-to-end example to show the concept, but there are lots of ways this could be improved depending on what you actually need. I hope that helps.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Archives
Categories

Enter your email address to follow this blog and receive notifications of new posts by email.

Join 2 other followers

%d bloggers like this: