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).
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.
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading;
- namespace AjaxProgressBarExample
- {
- /// <summary>
- /// Long Running Test Class.
- /// </summary>
- public class MyLongRunningClass
- {
- private static object syncRoot = new object();
- /// <summary>
- /// Gets or sets the process status.
- /// </summary>
- /// <value>The process status.</value>
- private static IDictionary<string, int> ProcessStatus { get; set; }
- /// <summary>
- /// Initializes a new instance of the <see cref="MyLongRunningClass"/> class.
- /// </summary>
- public MyLongRunningClass()
- {
- if (ProcessStatus == null)
- {
- ProcessStatus = new Dictionary<string, int>();
- }
- }
- /// <summary>
- /// Processes the long running action.
- /// </summary>
- /// <param name="id">The id.</param>
- public string ProcessLongRunningAction(string id)
- {
- for (int i = 1; i <= 100; i++)
- {
- Thread.Sleep(100);
- lock (syncRoot)
- {
- ProcessStatus[id] = i;
- }
- }
- return id;
- }
- /// <summary>
- /// Adds the specified id.
- /// </summary>
- /// <param name="id">The id.</param>
- public void Add(string id)
- {
- lock (syncRoot)
- {
- ProcessStatus.Add(id, 0);
- }
- }
- /// <summary>
- /// Removes the specified id.
- /// </summary>
- /// <param name="id">The id.</param>
- public void Remove(string id)
- {
- lock (syncRoot)
- {
- ProcessStatus.Remove(id);
- }
- }
- /// <summary>
- /// Gets the status.
- /// </summary>
- /// <param name="id">The id.</param>
- public int GetStatus(string id)
- {
- lock (syncRoot)
- {
- if (ProcessStatus.Keys.Count(x => x == id) == 1)
- {
- return ProcessStatus[id];
- }
- else
- {
- return 100;
- }
- }
- }
- }
- }
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).
- using System;
- using System.Linq;
- using System.Web.Mvc;
- namespace AjaxProgressBarExample.Controllers
- {
- /// <summary>
- /// Home Controller.
- /// </summary>
- [HandleError]
- public class HomeController : Controller
- {
- /// <summary>
- /// Index Action.
- /// </summary>
- public ActionResult Index()
- {
- ViewData["Message"] = "Ajax Progress Bar Example";
- return View();
- }
- delegate string ProcessTask(string id);
- MyLongRunningClass longRunningClass = new MyLongRunningClass();
- /// <summary>
- /// Starts the long running process.
- /// </summary>
- /// <param name="id">The id.</param>
- public void StartLongRunningProcess(string id)
- {
- longRunningClass.Add(id);
- ProcessTask processTask = new ProcessTask(longRunningClass.ProcessLongRunningAction);
- processTask.BeginInvoke(id, new AsyncCallback(EndLongRunningProcess), processTask);
- }
- /// <summary>
- /// Ends the long running process.
- /// </summary>
- /// <param name="result">The result.</param>
- public void EndLongRunningProcess(IAsyncResult result)
- {
- ProcessTask processTask = (ProcessTask)result.AsyncState;
- string id = processTask.EndInvoke(result);
- longRunningClass.Remove(id);
- }
- /// <summary>
- /// Gets the current progress.
- /// </summary>
- /// <param name="id">The id.</param>
- public ContentResult GetCurrentProgress(string id)
- {
- this.ControllerContext.HttpContext.Response.AddHeader("cache-control", "no-cache");
- var currentProgress = longRunningClass.GetStatus(id).ToString();
- return Content(currentProgress);
- }
- }
- }
One interesting line you may have notices in the GetCurrentProgress method is the following:
- 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.
- <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
- <asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
- Home Page
- </asp:Content>
- <asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
- <h2>
- <%= Html.Encode(ViewData["Message"]) %></h2>
- <div>
- <a href="#" id="startProcess">Start Long Running Process</a>
- </div>
- <br />
- <div id="statusBorder">
- <div id="statusFill">
- </div>
- </div>
- <script type="text/javascript">
- var uniqueId = '<%= Guid.NewGuid().ToString() %>';
- $(document).ready(function(event) {
- $('#startProcess').click(function() {
- $.post("Home/StartLongRunningProcess", { id: uniqueId }, function() {
- $('#statusBorder').show();
- getStatus();
- });
- event.preventDefault;
- });
- });
- function getStatus() {
- var url = 'Home/GetCurrentProgress/' + uniqueId;
- $.get(url, function(data) {
- if (data != "100") {
- $('#status').html(data);
- $('#statusFill').width(data);
- window.setTimeout("getStatus()", 100);
- }
- else {
- $('#status').html("Done");
- $('#statusBorder').hide();
- alert("The Long process has finished");
- };
- });
- }
- </script>
- </asp:Content>
Also add a little css for styling the progress bar:
- #statusBorder
- {
- position:relative;
- height:5px;
- width:100px;
- border:solid 1px gray;
- display:none;
- }
- #statusFill
- {
- position:absolute;
- top:0;
- left:0;
- width:0px;
- background-color:Blue;
- height:5px;
- }
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