Client Side Progress Bar in ASP.NET MVC using jQuery UI, WebAPI and JsRender

Editorial note: This article has been modified to incorporate (to an extent) the comments by an avid reader Parag. Thanks Parag!

In a recent discussion, I was asked about progress bars to visually depict ongoing activity on the web page. The scenario was that a page makes an Ajax call to an external service, receives JSON data and then displays this data in a tabular fashion. However the requirement here is that the end-user must be notified via an indicator when records gets loaded.

Note: To show a deterministic progress bar with ‘nearly’ accurate progress one must know the minimum, maximum and the current value of the work done. This makes progress bars on the web slightly tricky. The server while doing a long running task must periodically update the client or the client must continuously poll server to get the status. In this examples we ‘simulate’ such a scenario where server first tells the client the maximum number of rows to be returned and then the client pulls a certain amount of data and updates the progress bar accordingly.

jQuery UI provides a nice progress bar plugin that simply take the current value of progress assuming minimum is 0 and maximum is 100%. In Visual Studio you can install jQueryUI from Nuget using the command

PM> install-package jquery.UI.Combined

This downloads the required CSS also.

In this sample, we are retrieving a table of data. Traditionally this translates to a UI Markup with a <table> and several <tr> and <td>.

However we will use templates to simply this activity. In this article, I have used a new Templating engine called JsRender. This is a JavaScript library which allows developer to define a boilerplate structure for UI requirement and then this structure can be reused to generate dynamic HTML. You can obtain JsRender.js from https://github.com/BorisMoore/jsrender . The Free DNC Magazine has a nice article on how to use JsRender.

Implementation

Let’s jump into the implementation now.

Step 1: Open Visual Studio 2012 and create a blank MVC project, name it as ‘MVC_JQuery_UI’. In this project, create a ‘Scripts’ folder and add the following jQuery files:
Jsrender.js : The JavaScript Library for JsRender.

Install jQuery using the Nuget Package Manager command

PM> install-package jquery

Install jQueryUI using the Nuget Package Manager command

PM> install-package jquery.UI.Combined.

Step 2: To define a Model layer, we will use the SQL Server Northwind database and its Order table. (Note you can download Northwind database from Nuget too using the command install-package Northwind.db).

Step 3: In the model folder, using the ADO.NET EF wizard, add the EF design as shown below:

adonet-ef
 
Step 4: Right-click on the controller folder and add the WEB API Controller as shown below (Note: This is new in ASP.NET MVC 4)

web-api-controller

This will add a controller class, which will add methods which provides JSON communication. Check out Using MS WebAPI to add API Support to Your Existing ASP.NET MVC Web Applications

Step 5: In the same controller, add a new Empty controller of the name ‘OrderListController’. You will get a class with an empty Index method. Here we are using an Empty controller because all the business logic will be implemented using jQuery.

Step 6: Add a new Empty Scaffolded Index View in the project by right-clicking on the Index method created in the Step 5. You will get the OrderList sub folder under View folder.

Step 7: Open Index.cshtml and add the following JavaScript and css references:

<link href="~/css/smoothness/jquery-ui-1.9.0.custom.css" rel="stylesheet" />
<script src="~/Scripts/jquery-1.8.2.js"></script>
<script src="~/Scripts/jquery-ui-1.8.24.min.js"></script>
<script src="~/Scripts/jsrender.js"></script>


Step 8: In the Page, add the following HTML (Note: Please read comments)

<form id="form1" runat="server">
    <!--Button to make an async call to WEB API-->
    <input type="button" value="GetData" id="btngetdata" />
    <!--The ProgressBar-->
    <div id="progressBar">
    </div>
    <div style="float: left; padding-left: 10px">
        <label id="completeCount"></label>
    </div>

    <br />
    <!--Table to Display Header-->
    <table style="border: solid 1px #808080; padding: 0; margin: 0; border-collapse: collapse" id="tblemp">
        <thead>
            <tr style="background-color: #dbd0d0">
                <td>OrderID</td>
                <td style="width: 100px">Customer ID</td>
                <td style="width: 100px">Employee ID</td>
                <td style="width: 100px">Order Date</td>
                <td style="width: 100px">Require Date</td>
                <td style="width: 100px">Shipped Date</td>
                <td style="width: 300px">Ship Via</td>
                <td style="width: 300px">Freight</td>
                <td style="width: 200px">Ship Name</td>
                <td style="width: 400px">Ship Address</td>
                <td style="width: 200px">Ship City</td>
                <td style="width: 200px">Ship Region</td>
                <td style="width: 200px">Ship PostalCode</td>
                <td style="width: 200px">Ship Country</td>
            </tr>
        </thead>
        <!--The Container to display the Dynamically generated Table-->
        <tbody id="customerInfoContainer">
        </tbody>
    </table>
</form>


Note: I have used inline css for simplicity but ideally it should be avoided and CSS must be placed in a separate file.

The above Html contains:
  • Button control ‘btngetdata’ used to make an Ajax Async call to WEB API.
  • Div with id ‘progressBar’, used to display Progress Bar.
  • A label with id ‘completeCount’ to show number of items rendered out of the total number of items
  • The Table Header e.g. OrderId etc.
  • The <tbody> with id as ‘customerInfoContainer’ , used to display the dynamically generated HTML for Tables using JsRender.
Step 9: In the Index.cshtml, add the following Html template for dynamically generating HTML table for displaying information. This defines JsRender tags for displaying data inside it:

<!--The Template created will be reused to generate Table.
This contain tags expression using {{:}} syntax-->
<script id="customerTemplate" src="../Scripts/jsrender.js"
type="text/x-jsrender">
  <tr style="border: solid 1px #808080">
      <td style="width:100">{{:OrderID}}</td>
      <td style="width:100">{{:CustomerID}}</td>
      <td style="width:100">{{:EmployeeID}}</td>
      <td style="width:100">{{:OrderDate}}</td>
      <td style="width:100">{{:RequiredDate}}</td>
      <td style="width:100">{{:ShippedDate}}</td>
      <td style="width:300">{{:ShipVia}}</td>
      <td style="width:200">{{:Freight}}</td>
      <td style="width:400">{{:ShipName}}</td>
      <td style="width:400">{{:ShipAddress}}</td>
      <td style="width:200">{{:ShipCity}}</td>
      <td style="width:200">{{:ShipRegion}}</td>
      <td style="width:200">{{:ShipPostalCode}}</td>
      <td style="width:200">{{:ShipCountry}}</td>
  </tr>
</script>

This template is used by JsRender to dynamically generate the final HTML. This approach reduces several lines of code and becomes easier to maintain later on.

Step 10: Add the following script which defines the Click event for the button. This event makes a call to the WEB API, retrieve the JSON data and then prints the data on the Page using Table.

<script type="text/javascript">
$(document).ready(function () {
var _this = this;
var offset = 0;
var fetch = 10;
var totalRecords = 0;
var custRecordsRendered = 0;
///The Button Click which makes an ajax call to Web API
$("#btngetdata").click(function () {
    offset = 0;
    custRecordsRendered = 0;
    $.ajax({
        type: 'GET',
        url: "/api/Orders",
        datatype: "application/json; charset=utf-8",
        success: function (data) {
            totalRecords = data;
            fetch = parseInt(totalRecords / 100);
            $("#progressBar").progressbar(0);
            alert(fetch);
            getDataAtOffset();
        }
    });
    function getDataAtOffset() {
        $.ajax({
            type: 'GET',
            url: "/api/Orders?offset=" + offset * fetch + "&fetchRecords=" + fetch,
            datatype: "application/json; charset=utf-8",
            success: function (customerData) {
                var customers = [];
                customers = getDataInArray(customerData);
                //Convert the JSON response into an Array
                var custCount = customers.length;
                for (var i = 0; i < custCount; i++) {
                    displayCustomerInformation($("#customerTemplate").render(customers[i]));
                    custRecordsRendered = custRecordsRendered + 1;
                }
                updateProgress();
                if (offset * fetch < totalRecords) {
                    offset = offset + 1;
                    getDataAtOffset();
                }
            }
        });
    }
});


//Render the Data
function displayCustomerInformation(ele) {
    $("#customerInfoContainer").append(ele);
}

///Method to convertt the JSON data into an array
function getDataInArray(array) {
    var cust = [];
    $.each(array, function (i, d) {
        cust.push(d);
    });
    return cust;
}

///Method to update the progressbar
function updateProgress() {
    var progress = $("#progressBar").progressbar("option", "value");
    if (progress < 100) {
        //Increase the value of the progress by 1
        $("#progressBar").progressbar("option", "value", progress + 1);
    }
    $("#completeCount").text(custRecordsRendered + "/" + totalRecords + " Records Rendered");
}

});
</script>


In the above code we are doing the following on the jQuery document ready event:

1. Initializing the counters.
2. Assigning the click handler to the ‘Get Data’ button.
3. In the click handler, we first do an AJAX call to get the total number of orders.
4. Based on the value returned, we calculate how many records we can fetch per query to server. (Here we have used a simple mechanism by diving the total by 100 for easy association with the Progress Bar value. However if you have a much larger dataset then the 100% calculation might be a little more involved).
5. We then call the getDataAtOffSet method. This uses the offset and fetches size counters to retrieve data from the server. The API Controller method to retrieve data is as follows

// GET api/Orders
public IEnumerable<Order> GetOrders(int offset, int fetchRecords)
{
    return db.Orders.OrderBy(o=>o.OrderID).Skip<Order>(offset).
        Take<Order>(fetchRecords).AsEnumerable();
}


6. To accommodate the input parameters to the GET method we define a route in the WebApiConfig.cs as follows

config.Routes.MapHttpRoute(
    name: "OrdersWithOffset",
    routeTemplate: "api/{controller}/{offset}/{fetchRecords}",
    defaults: new { offset = RouteParameter.Optional,
    fetchRecords = RouteParameter.Optional }
);


7. As we can see the getDataAtOffset method pulls only a part of the data. It thereafter uses JsRender to render the n records returned.
8. Once the rendering is complete, it updates the progress bar
9. Finally if checks if the offset has reached the count of the records. If not, it calls itself with the updated offset. This continues till all records have been retrieved.
10. The updateProgress method take care of updating the current record label too.

This wraps up the changes required.

Step 11: Now if we run the application and in the address bar type the below URL (which may differ on your machine): hxxp://localhost:51914/ordersList The result will be as shown below:

orders-list-empty
Click on the ‘GetData’ button and now each row gets added with a progress indicator as shown below:

orders-list-with-data

Disclaimer: This is not even close to a well-designed table, but with the use of simple CSS, you can beautify it further. You can also apply various CSS effects to the ProgressBar as per your requirements.

Conclusion

ProgressBar is a handy widget from the jQuery UI. It helps provide a more ‘live’ User Experience for the end-user. Download the source code from Github (click on the ZIP to download as .zip)

8 comments:

  1. Sorry to say Mahesh but the HTML your example is going to render is symantically wrong and for each tr, you are going to render a div so I am not sure how its going to show up on different browsers. Moreover, the progress bar is not really a progress bar here since its going to start showing a progress only after your ajax call has completed successfully. The progress bar is going from 0% to 100% in exactly 20000 milliseconds and most importantly if you have more than 100 orders to display it wont render anything beyond 100 rows. Last but not the least the word Responsive UI is misleading as i thought the term Responsive Web designing had to do more with the way things render on different devices and browsers. The one thing you have got right is use of WebAPI (which in turn points to a different article). These are just my observation. Though the article had information about How to use Stuff, it does not seem have been used Properly.

    ReplyDelete
  2. Hi Parag,

    Thanks for your very astute comments. The invalid HTML markup is an editorial mixup. We are deploying the patch soon.

    Apologies for using Responsive in a different context. We'll update the post to clarify.

    Thanks and Regards,
    Sumit.
    (Editor)

    ReplyDelete
  3. Hi Guys,

    Thanks Parag for your comments and thanks Sumit for pitching in despite celebrations at your end, to modify the article in time.

    The updated article has been posted here.

    Much appreciated!

    ReplyDelete
  4. I really appreciate taking a solid wack at such a wide scale problem for most of the .net world.

    ReplyDelete
  5. Anonymous: A constructive feedback is welcome.

    ReplyDelete
  6. In your article i show client site progress bar with example.and I prefer to learn by example than the content.

    ReplyDelete
  7. A very useful site with lots of interesting papers and articles. No doubt it will be very useful and invaluable to research work.

    ReplyDelete
  8. This was a good start for me. i ended up using Twitter bootstrap for my process bar and Hid the JQuery one. One thing I did notice is that this line always produced 100

    var progress = $("#progressBar").progressbar("option", "value");

    I commented out the if below it and it was working.

    ReplyDelete