Asynchronous processing in ASP.Net MVC with Ajax progress bar

I was asked the other day how to process a long running task asynchronously using ASP.Net MVC along with JQuery to update a progress bar on the view via Ajax.

There are many ways of accomplishing this type of multithreading, in this blog I’m going to document one of the simplest and easiest to implement (this solution is really for small apps).

image

Firstly, create a class that will manage the long running task – as this is a contrived example without the use of a database, the class is going to have static dictionary property that will store the unique key and status of each long running task. – the dictionary is used to allow for multiple users firing off individual long running tasks.

  1. using System.Collections.Generic;
  2. using System.Linq;
  3. using System.Threading;
  4. namespace AjaxProgressBarExample
  5. {
  6.  
  7.     /// <summary>
  8.     /// Long Running Test Class.
  9.     /// </summary>
  10.     public class MyLongRunningClass
  11.     {
  12.  
  13.         private static object syncRoot = new object();
  14.  
  15.         /// <summary>
  16.         /// Gets or sets the process status.
  17.         /// </summary>
  18.         /// <value>The process status.</value>
  19.         private static IDictionary<string, int> ProcessStatus { get; set; }
  20.  
  21.         /// <summary>
  22.         /// Initializes a new instance of the <see cref="MyLongRunningClass"/> class.
  23.         /// </summary>
  24.         public MyLongRunningClass()
  25.         {
  26.             if (ProcessStatus == null)
  27.             {
  28.                 ProcessStatus = new Dictionary<string, int>();
  29.             }
  30.         }
  31.  
  32.         /// <summary>
  33.         /// Processes the long running action.
  34.         /// </summary>
  35.         /// <param name="id">The id.</param>
  36.         public string ProcessLongRunningAction(string id)
  37.         {
  38.             for (int i = 1; i <= 100; i++)
  39.             {
  40.                 Thread.Sleep(100);
  41.                 lock (syncRoot)
  42.                 {
  43.                     ProcessStatus[id] = i;
  44.                 }
  45.             }
  46.             return id;
  47.         }
  48.  
  49.         /// <summary>
  50.         /// Adds the specified id.
  51.         /// </summary>
  52.         /// <param name="id">The id.</param>
  53.         public void Add(string id)
  54.         {
  55.             lock (syncRoot)
  56.             {
  57.                 ProcessStatus.Add(id, 0);
  58.             }
  59.         }
  60.  
  61.         /// <summary>
  62.         /// Removes the specified id.
  63.         /// </summary>
  64.         /// <param name="id">The id.</param>
  65.         public void Remove(string id)
  66.         {
  67.             lock (syncRoot)
  68.             {
  69.                 ProcessStatus.Remove(id);
  70.             }
  71.         }
  72.  
  73.         /// <summary>
  74.         /// Gets the status.
  75.         /// </summary>
  76.         /// <param name="id">The id.</param>
  77.         public int GetStatus(string id)
  78.         {
  79.             lock (syncRoot)
  80.             {
  81.                 if (ProcessStatus.Keys.Count(x => x == id) == 1)
  82.                 {
  83.                     return ProcessStatus[id];
  84.                 }
  85.                 else
  86.                 {
  87.                     return 100;
  88.                 }
  89.             }
  90.         }
  91.     }
  92. }

Now we have the long running class in place we need to create some controller actions to do the following:

  • Start the long running process
  • End the long running process
  • Get the current process status

To allow for the asynchronous processing of the long running task, we are going to a delegate so all the hard work is essentially handled by the .net framework.

So as an overview, the StartLongRunningProcess(string id) method accepts a unique string for creating an entry in the dictionary, instantiates an asynchronous delegate which points to the ProcessLongRunningAction method on the above class. When the long running tasks completes the EndLongRunningProcess(IAsyncResult result) method is called.

The EndLongRunningProcess(IAsyncResult result) method removes the unique string from the dictionary – mainly a clean up exercise.

The GetCurrentProgress(string id) content result method sole purpose is to return the current status of the long running process by looking it up in the dictionary (it’s a bit hacky but this is only for demo purposes).

  1. using System;
  2. using System.Linq;
  3. using System.Web.Mvc;
  4.  
  5. namespace AjaxProgressBarExample.Controllers
  6. {
  7.     /// <summary>
  8.     /// Home Controller.
  9.     /// </summary>
  10.     [HandleError]
  11.     public class HomeController : Controller
  12.     {
  13.         /// <summary>
  14.         /// Index Action.
  15.         /// </summary>
  16.         public ActionResult Index()
  17.         {
  18.             ViewData["Message"] = "Ajax Progress Bar Example";
  19.             return View();
  20.         }
  21.  
  22.         delegate string ProcessTask(string id);
  23.         MyLongRunningClass longRunningClass = new MyLongRunningClass();
  24.  
  25.         /// <summary>
  26.         /// Starts the long running process.
  27.         /// </summary>
  28.         /// <param name="id">The id.</param>
  29.         public void StartLongRunningProcess(string id)
  30.         {
  31.             longRunningClass.Add(id);            
  32.             ProcessTask processTask = new ProcessTask(longRunningClass.ProcessLongRunningAction);
  33.             processTask.BeginInvoke(id, new AsyncCallback(EndLongRunningProcess), processTask);
  34.         }
  35.  
  36.         /// <summary>
  37.         /// Ends the long running process.
  38.         /// </summary>
  39.         /// <param name="result">The result.</param>
  40.         public void EndLongRunningProcess(IAsyncResult result)
  41.         {
  42.             ProcessTask processTask = (ProcessTask)result.AsyncState;
  43.             string id = processTask.EndInvoke(result);
  44.             longRunningClass.Remove(id);
  45.         }
  46.  
  47.         /// <summary>
  48.         /// Gets the current progress.
  49.         /// </summary>
  50.         /// <param name="id">The id.</param>
  51.         public ContentResult GetCurrentProgress(string id)
  52.         {
  53.             this.ControllerContext.HttpContext.Response.AddHeader("cache-control", "no-cache");
  54.             var currentProgress = longRunningClass.GetStatus(id).ToString();
  55.             return Content(currentProgress);
  56.         }
  57.     }
  58. }

One interesting line you may have notices in the GetCurrentProgress method is the following:

  1. this.ControllerContext.HttpContext.Response.AddHeader("cache-control", "no-cache");

This is important as some browsers including IE cache the results of urls so without this line, each call to the method would return the same result instead of the incrementing progress.

The next thing to do is create the view – in this case I’m just going to use the index view itself.

Note: make sure you add a reference to the JQuery library.

The following javascript has two methods.

The first attaches an onclick event to the startProcess anchor tag.

It then calls the getStatus method().

The getStatus method uses ajax to call the GetCurrentProcess content result method on the controller every 100 milliseconds.

When the result gets to 100, the progress bar is hidden and an alert will pop up to show that it’s finished.

  1. <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
  2.  
  3. <asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
  4.     Home Page
  5. </asp:Content>
  6. <asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
  7.     <h2>
  8.         <%= Html.Encode(ViewData["Message"]) %></h2>
  9.     <div>
  10.         <a href="#" id="startProcess">Start Long Running Process</a>
  11.     </div>
  12.     <br />
  13.     <div id="statusBorder">
  14.         <div id="statusFill">
  15.         </div>
  16.     </div>
  17.  
  18.     <script type="text/javascript">
  19.  
  20.         var uniqueId = '<%= Guid.NewGuid().ToString() %>';
  21.  
  22.         $(document).ready(function(event) {
  23.             $('#startProcess').click(function() {
  24.                 $.post("Home/StartLongRunningProcess", { id: uniqueId }, function() {
  25.                     $('#statusBorder').show();
  26.                     getStatus();
  27.                 });
  28.                 event.preventDefault;
  29.             });
  30.         });
  31.  
  32.         function getStatus() {
  33.             var url = 'Home/GetCurrentProgress/' + uniqueId;
  34.             $.get(url, function(data) {
  35.                 if (data != "100") {
  36.                     $('#status').html(data);
  37.                     $('#statusFill').width(data);
  38.                     window.setTimeout("getStatus()", 100);
  39.                 }
  40.                 else {
  41.                     $('#status').html("Done");
  42.                     $('#statusBorder').hide();
  43.                     alert("The Long process has finished");
  44.                 };
  45.             });
  46.         }
  47.     
  48.     </script>
  49.  
  50. </asp:Content>

Also add a little css for styling the progress bar:

  1. #statusBorder
  2. {
  3.     position:relative;
  4.     height:5px;
  5.     width:100px;
  6.     border:solid 1px gray;
  7.     display:none;
  8. }
  9. #statusFill
  10. {
  11.     position:absolute;
  12.     top:0;
  13.     left:0;
  14.     width:0px;
  15.     background-color:Blue;
  16.     height:5px;
  17. }

 

You can download this solution at the following: http://weblogs.asp.net/blogs/seanmcalinden/Solutions/AjaxProgressBarExample.zip

I hope this is helpful to anyone starting out with some basic asynchronous asp.net mvc.

Kind Regards,

Sean McAlinden

http://www.asp-net-mvc.com/

15 Comments

  • I was trying the progress-bar with long background process, My ajax polling was stuck until the bkgrnd process was complete i even posted a question in stackoverflow about this.but finally your solution helped me solve my problem. thnx a lot!

    Now i am also hearing that this is not the best way as it steals threads from worker process? its better to use
    Asynchronous web handlers (IHttpAsyncHanddler) ? U said "There are many ways of accomplishing this type of multithreading" can you name them? may be i can compare the results as which solution is better for aspnet mvc

  • Hi SpiderDevil

    Asynchronous Delegates are often the recommended way of accomplishing this type of task as it safely uses threads from the thread pool - if you're working on real low latency software it would be worth investigating other methods but all in all, it really depends on the best tool for the job - there is no definitive best way.

    Here's a fairly good article that shows various implementations of a handler using different techniques including delegates and custom threads etc. http://msdn.microsoft.com/en-us/magazine/cc164128.aspx#S5

    I would recommend if you are new to threading - delegates are a pretty good way to go as you're less likely to get the unusual and often extremely difficult to find bugs.

    Glad the article helped.

    Kind Regards,

    Sean.

  • Thanks for the ControllerContext.HttpContext.Response.AddHeader("cache-control", "no-cache"); info. Firefox was refreshing my jquery link but IE8 was not. This fixed it!

  • Hi, these seems to work only on IE and not Firefox or Chrome, just tested it. Am I doing something wrong?

    Thanks

  • Hi, these seems to work only on IE and not Firefox or Chrome, just tested it. Am I doing something wrong?

  • Hi Hamish/ForMeToo,

    It should be working cross browser, it is likely to be something to do with the browser caching, depending on what version of jquery and mvc you're using you may need to play around with it a bit - try using the $.ajax jquery function instead of the $.get and pass the setting cache: false - hopefully this should solve the issue.

    Kind Regards,
    Sean.

  • I did this fix to my script to get it working

    instead of
    $('#statusFill').width(data);

    use
    $('#statusFill').css({'width':data.toString()+"px"});

  • in this example, the code to handle the threading and progress are part of the class with the long running task.

    how would I do this with a task that is part of another independent class.

    I don't want to add all the code into the class with the long running process (except obviously it needs to be able to update the progress).

    thanks

  • @mnewmanov

    I would think that because the ProcessStatus Dictionary is static, the MyLongRunningClass object could be instantiated anywhere and still have access to the same ProcessStatus object.

    I agree with you that the method ProcessLongRunningAction does not belong in the MyLongRunningClass object. Instead I would have a method
    public void SetStatus(string id, int value) which would set the ProcessStatus value.


    Then the controller would look like
    public ActionResult DoLongRunningTask(string id)
    {
    MyLongRunningClass longRunningClass = new MyLongRunningClass();
    longRunningClass.Add(id);
    int percentDone = 0;
    while ( percentDone < 100 )
    {
    percentDone = doSomeWork();
    longRunningClass.SetStatus(id,percentDone);
    )
    longRunningClass.Remove(id);
    return View();
    }

    The GetCurrentProgress part of the controller would stay the same.

  • Hi rtanenbaum,

    Thanks for answering that one.

    Kind Regards,
    Sean.

  • great article
    Thanks

  • how to redirect to another url after successful completion.

  • Currently it sounds like Drupal is the best blogging platform out there right
    now. (from what I've read) Is that what you're using on your blog?

  • Thanks, works like a charm !

  • Occasional the callback method doesn't get called. Is there a reason for this?

Comments have been disabled for this content.