Whenever there is a pause of a few seconds for a heavier process to be executed, it
It is important for the client not to feel deactivated when there is a pause of a few seconds in a heavier process that is been executed. Know how to avoid despair while you wait… with AngularJS and .NET .
Whenever there is a pause of a few seconds for a heavier process to be executed, it is important for the client not to feel deactivated. The most user-friendly fix for this would be to know what is happening at that moment.
To do this, we use a Progress Bar to indicate the status of the operation, so that the user may continue to use other options on the page. Let´s discuss how to approach the solution:
In this example, the user needs to save a movie to the base, an operation that takes about 10 seconds.
- The client performs a POST with the content to the server (“/ movies”) and it launches the process.
- The server responds to the request with 202 –´Accepted´ indicating that the request has been accepted, but the process has not yet been completed. In addition, the server modifies the header of Content-Location, which will indicate the URL to which must be accessed to know the status of the operation.
- The client executes a polling to the received address, showing the percentage of progress in the UI.
For starters, let’s look at the code for the class Progress.cs:
public class Progress { public event EventHandler ProcessComplete; public event EventHandler<ProgressChangedEventArgs> ProgressChanged; public double ProgressPercent { get; set; } public bool IsRunning { get; set; } public bool IsCompleted { get; set; } public string Id { get; set; } public string ProgressMessage { get; set; } private Task Task { get; set; } public void StartProcessing(string idProcess) { this.Id = idProcess; this.IsCompleted = false; this.IsRunning = true; this.ProgressPercent = 0; this.ProgressMessage = "Starting..."; Task = Task.Factory.StartNew(() => { StartLongRunningProcess(); }); } private void StartLongRunningProcess() { Thread.CurrentThread.Name = this.Id; this.IsRunning = true; this.ProgressMessage = "Saving"; for (int i = 0; i < 100; i++) { if (i == 50) { this.ProgressMessage = "Still saving"; } else { if (i == 85) { this.ProgressMessage = "Almost done"; } } this.UpdatePercent(i); Thread.Sleep(500); } this.UpdatePercent(100); Thread.Sleep(1000); if (this.ProcessComplete != null) { this.ProcessComplete(this, new EventArgs()); Thread.Sleep(8000); } this.IsRunning = false; } private void UpdatePercent(double newPercent) { this.ProgressPercent = newPercent; if (newPercent == 100) { this.IsCompleted = true; this.ProgressMessage = "Finally! Process complete!"; } if (this.ProgressChanged != null) { this.ProgressChanged(this, new ProgressChangedEventArgs(newPercent, this.IsCompleted)); } } }
We can see that our class contains properties like ‘ IsCompleted ‘ which we will use in the front for polling. The StartProcessing method will receive a generated id, set the initial values and launch the process.
StartLongRunningProcess will simulate this heavier process. In this case, use several Thread.sleep to help slow down. Later, UpdatePercent, as its name implies, will update the percentage of our process.
Each step has a custom message to the UI is a little more user-friendly and doesn´t just show a number.
Now, in our controller we have:
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Web.Http; using BackendAngular.DAO; using BackendAngular.Models; using MongoDB.Bson; namespace BackendAngular.Controllers { [RoutePrefix("api")] public class MoviesController : ApiController { private static Progress progress = new Progress(); [HttpPost] [Route("movies")] public HttpResponseMessage Post([FromBody]Movie movie) { progress.StartProcessing(moviesDAO.CreateMovie(movie)); //moviesDAO.CreateMovie(movie) nos devuelve el ID generado para esta operacion HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Accepted); string uri = "http://localhost:51125/api/movies/progress/" + progress.Id; response.Content = new StringContent(string.Empty, Encoding.Unicode); //Creamos un Content para nuestro response (asi podremos editar los headers) response.Content.Headers.Add("Content-Location", uri); response.Content.Headers.Add("Access-Control-Expose-Headers", "Content-Location"); //agregamos este header para que nuestro front pueda acceder a Content-Location return response; } [HttpGet] [Route("movies/progress/{id}")] public Progress GetProgress(string id) { if (progress != null && progress.IsRunning) { return progress; } return null; }
You need to add the header “Access -Control- Expose- Headers” since the only headers accessible by default are:
- Cache- Control
- Content-Language
- Content -Type
- Expires
- Last -Modified
- Pragma
With that we would be ready in the backend. We will use the Postman for operation:
We perform POST and get the following response:
And if we execute a GET to the direction indicated we obtain:
Now, for our Front, we will use Angularjs to create the polling that will go on processing our requests and also update the progress bar.
For the graphics, we will use the directive: Bootstrap from Progress Bar
So in our view we have:
<div> <div class="form-group"> <label>Titulo:</label> <input type="text" ng-model="newMovie.title" class="form-control"></input> </div> <div class="form-group"> <label>Director:</label> <input type="text" ng-model="newMovie.director" class="form-control"></input> </div> <div class="form-group"> <label>Genero:</label> <input type="text" ng-model="newMovie.genre" class="form-control"></input> </div> <div class="form-group"> <label>Duracion</label> <input type="text" ng-model="newMovie.runningTime" class="form-control"></input> </div> <div class="form-group"> <label>Estreno:</label> <input type="text" ng-model="newMovie.releaseDate" class="form-control"></input> </div> </div> <button class="btn btn-success" ng-click="saveMovie(newMovie)">Guardar</button> <progressbar class="progress-striped active" animate="false" max="100" value="progressValue" type="{{type}}"> {{progressValue}}/100<br/> {{message}} </progressbar>
Where we have a small form for the POST of the movie and our bar, which would be something like this:
And our controller would look like this:
angular.module('stackAngularJsApp').controller('VisualizacionProgressBarCtrl', function ($scope, $timeout, $http) { 'use strict'; $scope.newMovie = {}; $scope.progressLink = ''; $scope.type = 'info'; $scope.saveMovie = function(movie){ var jsonMovie = angular.toJson(movie); $http({ method: 'POST', url: $scope.server.url + 'movies',//'http://localhost:8080/stack-angular-1.0-SNAPSHOT/movies/create', headers: { 'Content-type' : 'application/json' }, data : { movie : jsonMovie } }).success(function(data, status, headers){ var progressLink = headers('Content-Location'); loadProgress(progressLink); }).error(function() { }); }; var loadProgress = function(progressURL){ var value = $http({ method : 'GET', url : progressURL }); value.success(function(data){ $scope.progressValue = data.ProgressPercent; $scope.message = data.ProgressMessage; if(data.IsCompleted){ $timeout.cancel(timer); $scope.type = 'success'; } }); var timer = $timeout(function() { loadProgress(progressURL); }, 1000); }; });
Where we have our saveMovie function the URL function that performs the initial post and keeps the URL obtained from Content-Location header, to then later call loadProgress, which has a $timeout every 1 second where it finalizes the GET, obtaining the state of progress, and once we’re finishing the process, I end the $ timeout.