Skip to content

Progress Reporting in Web Application using HTTP Request API Endpoint

adriancs edited this page Aug 5, 2025 · 10 revisions

Part 2: Progress Reporting in Web Application using HTTP Request/API Endpoint

Main Content:


This is a continuous of the Progress Reporting Series with MySqlBackup.NET

In Web Application, there is a distinct separation of Frontend and Backend.

The Frontend refers to the HTML (JavaScript+CSS) page content that shown in the web browser.

The Backend refers to the web server.

There are a few ways of communication method between Frontend and Backend. The methods that we'll be discussing in the series are:

Communication Method Description Direction Frontend to Backend Backend to Frontend
HTTP Request / API Endpoint RESTful API, or simply just API Request/Response Initiates Passive Response
Web Socket A single constant connection that enables bi-direction communication between Frontend and Backend. Real-time updates, fast. Bi-directional
Server-Sent Events (SSE) A single constant connection from Backend to Frontend. One-way communication, Backend to Frontend only. Lightweight, real-time updates, fast. One-way

This article, we'll first explore the HTTP Request + API Endpoint method. We'll demonstrate the working mechanism in Vanilla ASP.NET Web Forms, but the architecture and patterns can be easily migrated to MVC, .NET Core, etc...

Vanilla ASP.NET Web Forms Architecture - Zero ViewState, No Server Control, No User Control, No UpdatePanel. Using just pure HTML, JavaScript and CSS

Basics of HTTP Request and API Endpoint Handling

We'll do a quick run through, then we'll proceed to the progress reporting thing.

First the Frontend, the HTTP request is normally execute through FetchAPI or AJAX (XMLHttpRequest).

Using FetchAPI to Send HTTP Request (using Form as body payload)

let urlApi = "/apiBackup";

const formData = new FormData();
formData.append("action", "say_welcome");
formData.append("name", "Mr. Anderson");

const result = await fetch(urlApi, {
    method: "POST",
    body: formData,
    credentials: "include" // require user login authentication
});

// -------------------------
// Server returns back data
// -------------------------

if (result.ok) {

    // --------------------------------------
    // example 1: server returns pure text
    // --------------------------------------
    
    let text = await result.text();
    alert(text);

    // --------------------------------------
    // example 2: server return json formatted string
    // --------------------------------------
    
    let jsonObject = await result.json();
    let msg = jsonObject.Message;
    alert(msg);
    
    // Sample output:
    // "Mr. Anderson, welcome back"
}

Using FetchAPI to Send HTTP Request (using json as body payload)

const jsonBody = {
        action: "say_welcome",
        name: "Mr. Anderson"
    };

const result = await fetch(urlApi, {
    method: "POST",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify(jsonBody),
    credentials: "include" // require user login authentication
});

if (result.ok) {

    let text = await result.text();
    alert(text);
    
    // Sample output:
    // "Mr. Anderson, welcome back"
}

Using FetchAPI with QueryString to Send HTTP GET Request

const params = new URLSearchParams({
    action: "say_welcome",
    name: "Mr. Anderson"
});

const urlQueryString = `${urlApi}?${params.toString()}`;

// sample output: "/apiBackup?action=say_welcome&name=Mr.%20Anderson"

const result = await fetch(urlQueryString, {
    method: "GET",
    credentials: "include" // require user login authentication
});

if (result.ok) {

    // ....
}

Using AJAX (XMLHttpRequest) to HTTP Request (form post)

const formData = new FormData();
formData.append("action", "say_welcome");
formData.append("name", "Mr. Anderson");

const xhr = new XMLHttpRequest();
xhr.open("POST", urlApi, true);
xhr.withCredentials = true; // equivalent to credentials: "include"

xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
    
        let msg = xhr.responseText;
        alert(msg);
        
        // Sample output:
        // "Mr. Anderson, welcome back"
    } 
};

xhr.onerror = function() {
    // network error
};

xhr.send(formData);

Now, let's switch to the Backend. In ASP.NET Web Forms, create a new blank page. Let's name the page as apiBackup.aspx. Assume we have route this page from this:

/pages/api/apiBackup.aspx

to

/apiBackup

The initial Frontend markup will looks something like this:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiBackup.aspx.cs" Inherits="myweb.apiBackup" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
        </div>
    </form>
</body>
</html>

Deletes all frontend markup, leave only the first line, the page directive declaration:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiBackup.aspx.cs" Inherits="myweb.apiBackup" %>

Now, go to code behind, initial code:

namespace myweb
{
    public partial class apiBackup : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }
    }
}

We have already explore a few methods of how the Frontend JavaScript can send the HTTP request.

If the Frontend is sent by using Form Data or Query String, we can capture the values using C# Request with the Key-Value access:

public partial class apiBackup : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string action = Request["action"] + "";
        
        if (action == "say_welcome")
        {
            string name = Request["name"] + "";
            
            string msg = $"{name}, welcome back";
            
            Response.Write(msg);
            
            // output:
            // "Mr. Anderson, welcome back"
        }
    }
}

Write the response as JSON:

Include the JSON Serialization Library:

using System.Text.Json;
using System.Text.Json.Serialization;

The code behind (for JSON)

public partial class apiBackup : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string action = Request["action"] + "";
        
        if (action == "say_welcome")
        {
            string name = Request["name"] + "";
            
            string msg = $"{name}, welcome back";
            
            // output:
            // "Mr. Anderson, welcome back"
            
            // create an anonymous object
            var result = new 
            {
                Message = msg
            };
            
            // convert anonymous object into JSON
            string json = JsonSerializer.Serialize(result);
            Response.ContentType = "application/json";
            Response.Write(json);
        }
    }
}

If the Frontend sends JSON formatted string to the Backend

public partial class apiBackup : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // load the text, a json formatted string
        string content = "";
        using (var reader = new StreamReader(Request.InputStream))
        {
            content = reader.ReadToEnd();
        }

        // convert the JSON string into C# class object
        var actionInfo = JsonSerializer.Deserialize<ActionInfo>(content)
        
        if (actionInfo.action == "say_welcome")
        {
            string name = actionInfo.name;
            
            string msg = $"{name}, welcome back";
            
            // output:
            // "Mr. Anderson, welcome back"
            
            // create an anonymous object
            var result = new 
            {
                Message = msg
            };
            
            // convert anonymous object into JSON
            string json = JsonSerializer.Serialize(result);
            Response.ContentType = "application/json";
            Response.Write(json);
        }
    }
}

// definition of the class object
public class ActionInfo
{
    public action { get; set; }
    public name { get; set; }
}

That's a quick basic introductory of sending HTTP request to the Backend server, and a round trip of Backend handling and returning response to the Frontend.

Now, let's build the progress reporting app using above principles in Vanilla ASP.NET Web Forms for Backup/Restore of MySqlBackup.NET.

Building Progress Reporting in Web Application (Vanilla ASP.NET Web Forms) Using HTTP Request

Build the Frontend first or Backend first, it's highly subjective to personal style of software engineering. But I always prefer to begin from Backend first, it builds a foundation for the Frontend to match or to align with. We'll define all the possible "actions" that allow the Frontend to call. If doing the Frontend without pre-build Backend, the feeling will be like landing on an empty foundation or doing a hallucination that might ends up spaghetti code structure. Again, this is highly subjective to personal style.

An overview of a backup process:

Example of MySqlBackup Progress Reporting UI

The architecture of the whole progress can be divided into few area of components.

  • The Control Centre, The Intermediary Data Caching
  • The Backend API C# handler
  • MySqlBackup, the main process
  • The Frontend (HTML, JavaScript, CSS)

Let's start with writing the Intermediary Data Caching class object:

public class TaskInfo
{
    public int ApiCallIndex { get; set; }

    public int TaskId { get; set; }
    public int TaskType { get; set; } // 1 = Backup, 2 = Restore
    public string FileDownloadWebPath { get; set; }

    // Task Control Centre
    public bool IsCompleted { get; set; }
    public bool IsCancelled { get; set; }
    public bool RequestCancel { get; set; }
    public bool HasError { get; set; }
    public string ErrorMsg { get; set; }

    public int PercentCompleted { get; set; }

    // Backup Progress Status
    public int TotalTables { get; set; }
    public int CurrentTableIndex { get; set; }
    public string CurrentTableName { get; set; }
    public long TotalRows { get; set; }
    public long CurrentRows { get; set; }
    public long TotalRowsCurrentTable { get; set; }
    public long CurrentRowsCurrentTable { get; set; }

    // Restore Progress Status
    public long TotalBytes { get; set; }
    public long CurrentBytes { get; set; }
}

How to store the Intermediary Data Caching class object.

Scenario 1: If the ASP.NET Web Forms application is running in a single Application Pool with single Worker Process:

  • Use ConcurrentDictionary. A thread safe class object. Easy to manage. The ConcurrentDictionary will not survive a server restart or resource recycling of application pool, yes, but so do the task too will be terminated as well in such circumstances. In most cases, this is not much of a concern.

Scenario 2: If the application is running in "Web Garden", a single Application Pool but with more than one Worker Process:

  • Use SQLite, accessible by different isolated worker process session, that always points to the same physical file source.

Scenario 3: If the application is running in "Web Farms", deployed across multiple web servers, perhaps manage by a load balancer:

  • Use another separate dedicated MySQL Server, or another dedicated database within the same MySQL server.

Next, we'll build/write the Backend API C# handler, together with the Intermediary Data Caching method.

How many action handlers are needed?

First, we need action handlers to initiate backup and restore process.

Another action handler to stop the process,

and another handler to let the Frontend to get the progress status.

So there are 4 basic handlers. Let's design the action name or command name for them:

  • start_backup
  • start_restore
  • stop_task
  • get_status

Sounds good?

Ok, continue the Backend boiler plate code structure:

// import the MySQL connector

// for MySqlConnector (MIT)
using MySqlConnector;

// for mysql.data (Oracle Connector/NET)
using MySql.Data.Client;

// for Devart Express
using Devart.Data.MySql;

// import other supporting tools/libraries
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

public partial class apiBackup : System.Web.UI.Page
{
    // assuming your application is run on single Application Pool with single worker process
    // use a static ConcurrentDictionary to store the Intermediary Data Caching class object
    // accessible by any threads within the whole application
    // a thread safe object
    static ConcurrentDictionary<int, TaskInfo> dicTask = new ConcurrentDictionary<int, TaskInfo>();

    protected void Page_Load(object sender, EventArgs e)
    {
        // in real production, you'll most probably has a user login authentication check
        if (!IsUserAuthenticated())
        {
            // HTTP Error 401 Unauthorized
            Response.StatusCode = 401;
            Response.Write("Unauthorized Access");
            return;
        }

        try
        {
            // the main entry point of HTTP request
            
            // the following setup requires the Frontend to use either Form Post or Query String
            
            string action = (Request["action"] + "").Trim().ToLower();

            // defines all the actions that you wish to have here
            
            switch (action)
            {
                case "start_backup":
                    Backup();
                    break;
                case "start_restore":
                    Restore();
                    break;
                case "stop_task":
                    Stop();
                    break;
                case "get_status":
                    GetStatus();
                    break;
                case "":
                    // HTTP Error 400 Bad Request
                    Response.StatusCode = 400;
                    Response.Write("Empty Request");
                    break;
                default:
                    // HTTP Error 406 Not Acceptable
                    Response.StatusCode = 406;
                    Response.Write($"Unsupported action: {action}");
                    break;
            }
        }
        catch (Exception ex)
        {
            // capture any error
            // HTTP Error 500 Internal Server Error
            Response.StatusCode = 500;
            Response.Write(ex.Message);
        }
    }

    bool IsUserAuthenticated()
    {
        // User authentication logic here
        // Check if user is logged in and has backup permissions
        // TEMPORARY - for testing and debugging use

        if (Session["user_login"] == null)
        {
            return false;
        }

        return true;
    }

    void Backup() 
    {

    }

    void Restore()
    {

    }

    void GetStatus()
    {

    }

    void Stop()
    {

    }
}

Let's build the first routine - Backup

public void Backup()
{
    try
    {
        int taskId = GetNewTaskId();

        // create a task info - The Data Control Centre / Intermediary Data Caching Object
        TaskInfo taskInfo = new TaskInfo();
        taskInfo.TaskId = taskId;
        taskInfo.TaskType = 1; // backup

        // store the task info to the global dictionary
        // for tracking/tracing
        dicTask[taskId] = taskInfo;

        // Multi-Thread - Run the process in another thread (run in background)
        _ = Task.Run(() => { BeginBackup(taskId); });

        // the code will proceed to this line without waiting the Backup process
        
        // create an anonymous object
        var result = new
        {
            TaskId = taskId
        };

        // convert it into JSON formatted string
        var json = JsonSerializer.Serialize(result);
        
        // immediately send the task id to Frontend
        Response.ContentType = "application/json";
        Response.Write(json);
        
        // by this point, the Frontend will capture the task id and 
        // begin the progress status monitoring
    }
    catch (Exception ex)
    {
        // HTTP Error 500 Internal Server Error
        Response.StatusCode = 500;
        Response.Write(ex.Message);
    }
}

// this will be running in background, another thread
void BeginBackup(int taskId)
{
    // get task info associates with this task id
    if (dicTask.TryGetValue(taskId, out var taskInfo))
    {
        try
        {
            // in ASP.NET, "/App_Data" is a protected directory
            // that can only be accessed by Backend server
            // normall HTTP request to this folder is restricted, it will be accessed denied
            string folder = Server.MapPath("~/App_Data/backup");
            Directory.CreateDirectory(folder);
            
            string fileName = $"backup-{DateTime.Now:yyyy-MM-dd_HHmmssd}.sql";
            string sqlFile = Server.MapPath($"/App_Data/backup/{fileName}");

            using (var conn = config.GetNewConnection())
            using (var cmd = conn.CreateCommand())
            using (var mb = new MySqlBackup(cmd))
            {
                conn.Open();
                
                // report progress 4 times per second
                mb.ExportInfo.IntervalForProgressReport = 250; // milliseconds
                
                // subscribe progress change event, pass the task id to the event handler
                mb.ExportProgressChanged += (sender, e) => Mb_ExportProgressChanged(sender, e, taskId);
                
                mb.ExportToFile(sqlFile);
            }
            
            // passing this point, indicates the process is ended
            // it's either successfully completed normally,
            // or it is cancelled by user request
            
            // detect the task status
            
            if (taskInfo.RequestCancel)
            {
                taskInfo.IsCancelled = true;
                try
                {
                    // clean up unfinished file
                    File.Delete(sqlFile);
                }
                catch { }
            }
            else
            {
                // backup success, tell the Frontend the download path
                // use another page to transmit the actual file path for download
                // you might need to implement some authentication on that page
                
                taskInfo.FileDownloadWebPath = $"/downlaod-backup-file?filename={filename}";
            }
        }
        catch (Exception ex)
        {
            taskInfo.HasError = true;
            taskInfo.ErrorMsg = ex.Message;
        }

        // Finally, mark the task info as completion
        // This will let the Frontend to stop it's timer to pull for progress status data
        taskInfo.IsCompleted = true;
    }
}

void Mb_ExportProgressChanged(object sender, ExportProgressArgs e, int taskId)
{
    // get task info associates with this task id
    if (dicTask.TryGetValue(taskId, out var taskInfo))
    {
        taskInfo.TotalTables = e.TotalTables;
        taskInfo.CurrentTableIndex = e.CurrentTableIndex;
        taskInfo.CurrentTableName = e.CurrentTableName;
        taskInfo.TotalRows = e.TotalRowsInAllTables;
        taskInfo.CurrentRows = e.CurrentRowIndexInAllTables;
        taskInfo.TotalRowsCurrentTable = e.TotalRowsInCurrentTable;
        taskInfo.CurrentRowsCurrentTable = e.CurrentRowIndexInCurrentTable;
        taskInfo.PercentCompleted = CalculatePercent(e.TotalRowsInAllTables, e.CurrentRowIndexInAllTables);
        
        // detect user cancellation request
        // this boolean is marked in "stop_task" action HTTP request handler
        // will be explained after this section
        if (taskInfo.RequestCancel)
        {
            // signal MySqlBackup to stop process
            ((MySqlBackup)sender).StopAllProcess();
        }
    }
}

Handling the Stop HTTP request:

void Stop()
{
    try
    {
        // get the task id
        if (int.TryParse(Request["taskid"] + "", out int taskid))
        {
            // get the task info
            if (dicTask.TryGetValue(taskid, out TaskInfo taskInfo))
            {
                // set the boolean flag for signaling the cancellation request
                taskInfo.RequestCancel = true;

                // do not modify response code
                // default response status = 200 = ok/success
                // This implies a succesfful request
                // anything falls out of this scope will be an error request code
                // some software engineering practice might suggest
                // to at lease return a success code, but that's optional in this context
            }
            else
            {
                // HTTP Error 404 Not Found
                Response.StatusCode = 404;
                Response.Write($"Task ID not found: {taskid}.");
            }
        }
        else
        {
            // HTTP Error 404 Not Found
            Response.StatusCode = 400;
            Response.Write($"Empty or Invalid Task ID.");
        }
    }
    catch (Exception ex)
    {
        // HTTP Error 500 Internal Server Error
        Response.StatusCode = 500;
        Response.Write(ex.Message);
    }
}

Handling Get Status Request:

void GetStatus()
{
    try
    {
        // get the task id
        if (int.TryParse(Request["taskid"] + "", out int taskid))
        {
            // get the api call index
            int.TryParse(Request["api_call_index"] + "", out int api_call_index);

            // the api call index will be incremented at the Frontend
            // each time a get status request is called
            // this is to prevent late echo replies, more on this later
            
            // get the task info
            if (dicTask.TryGetValue(taskid, out TaskInfo taskInfo))
            {
                // mark the call index for this batch of info/data
                taskInfo.ApiCallIndex = api_call_index;
                
                // convert the task info into JSON formatted string
                string json = JsonSerializer.Serialize(taskInfo);
                
                // send the json to the Frontend
                Response.ContentType = "application/json";
                Response.Write(json);
            }
            else
            {
                // HTTP Error 404 Not Found
                Response.StatusCode = 404;
                Response.Write($"Task ID not found: {taskid}.");
            }
        }
        else
        {
            // HTTP Error 400 Bad Request
            Response.StatusCode = 400;
            Response.Write($"Invalid Task ID");
        }
    }
    catch (Exception ex)
    {
        // HTTP Error 500 Internal Server Error
        Response.StatusCode = 500;
        Response.Write(ex.Message);
    }
}

About Api Call Index

Under normal circumstances, the sequence of HTTP request and response will be as follow:

Request Call 1

Response for Call 1 << 10% // normal expected return sequence

Request Call 2

Response for Call 2 << 20%

Request Call 3

Response for Call 3 << 30%

Due to the network latency issue, there might occur a late echo reply, for example:

Request Call 1

... due to sudden unknown latency, late returning

Request Call 2

Response for Call 2 << 20%

Request Call 3

Response for Call 3 << 30%

// late return
Response for Call 1 << 10%  ... late echo, ignore this batch of data

So, the api call index is used for prevention of late replies. If there is a late reply, the Frontend will simply ignore that response.

Next, continue to handle the last action handler - Restore:

void Restore()
{
    try
    {
        // in this process, the Frontend will submit a SQL file
        
        // detect file submission
        if (Request.Files.Count == 0 || Request.Files[0].ContentLength == 0)
        {
            Response.StatusCode = 400;
            Response.Write("No file uploaded");
            return;
        }

        // get the file
        var file = Request.Files[0];
        
        // get the task id
        int newTaskId = GetNewTaskId();

        TaskInfo taskInfo = new TaskInfo();
        taskInfo.TaskId = newTaskId;
        taskInfo.TaskType = 2; // restore

        // save the task info to global dictionary
        dicTask[newTaskId] = taskInfo;

        // Save and process uploaded file
        string folder = Server.MapPath("~/App_Data/backup");
        Directory.CreateDirectory(folder);
        
        string fileName = $"restore-{DateTime.Now:yyyyMMdd_HHmmss}.sql";
        string filePath = Server.MapPath($"~/App_Data/backup/{fileName}");

        file.SaveAs(filePath);

        // run the process in another separated thread
        _ = Task.Run(() => BeginRestore(newTaskId, sqlFilePath));

        // return task id immediately to Frontend
        
        // create an anonymous object
        var result = new
        {
            TaskId = taskId
        };

        // convert it into JSON formatted string
        var json = JsonSerializer.Serialize(result);
        Response.ContentType = "application/json";
        Response.Write(json);
        
        // by this point, the Frontend will capture the task id and 
        // begin the progress status monitoring
    }
    catch (Exception ex)
    {
        // HTTP Error 500 Internal Server Error
        Response.StatusCode = 500;
        Response.Write(ex.Message);
    }
}

// running in another thread
void BeginRestore(int thisTaskId, string filePathSql)
{
    if (dicTask.TryGetValue(thisTaskId, out TaskInfo taskInfo))
    {
        try
        {
            using (var conn = config.GetNewConnection())
            using (var cmd = conn.CreateCommand())
            using (var mb = new MySqlBackup(cmd))
            {
                conn.Open();
                
                // better real-time accurancy for progress reporting
                mb.ImportInfo.EnableParallelProcessing = false;
                
                // 250 milliseconds, report 4 times per second
                mb.ImportInfo.IntervalForProgressReport = 250;
                
                // subscribe the progress change event, passing the task id to the event handler
                mb.ImportProgressChanged += (sender, e) => Mb_ImportProgressChanged(sender, e, thisTaskId);
                
                mb.ImportFromFile(filePathSql);
            }

            if (taskInfo.RequestCancel)
            {
                taskInfo.IsCancelled = true;
            }
        }
        catch (Exception ex)
        {
            taskInfo.HasError = true;
            taskInfo.ErrorMsg = ex.Message;
        }

        string filename = Path.GetFileName(filePathSql);

        taskInfo.FileDownloadWebPath = $"/downlaod-backup-file?filename={filename}";
        taskInfo.IsCompleted = true;
    }
}

void Mb_ImportProgressChanged(object sender, ImportProgressArgs e, int thisTaskId)
{
    // get the task info from global dictionary
    if (dicTask.TryGetValue(thisTaskId, out var taskInfo))
    {
        taskInfo.TotalBytes = e.TotalBytes;
        taskInfo.CurrentBytes = e.CurrentBytes;
        taskInfo.PercentCompleted = CalculatePercent(e.TotalBytes, e.CurrentBytes);
        
        // detection of cancellation request
        if (taskInfo.RequestCancel)
        {
            // receive signalling for termination, stop all process
            ((MySqlBackup)sender).StopAllProcess();
        }
    }
}

Building the Frontend

In matching the available actions provided by the Backend, we can build at least four functions:

The Frontend JavaScript:

// -------------------
// main functions
// -------------------

async function startBackup() {

}

async function startRestore() {

}

async function stopTask() {

}

async function getStatus() {

}

// -------------------
// utility functions
// -------------------

function updateUiValues(jsonObject) {

}

function startProgressUpdateTimer() {
    // start the interval timer to get the progress status from Backend
}

function stopProgressUpdateTimer() {
    // stop the interval timer
}

The first 3 functions can be binded into three HTML buttons such as:

<button type="button" onclick="startBackup();">Backup</button>
<button type="button" onclick="startRestore();">Restore</button>
<button type="button" onclick="stopTask();">Stop</button>

Another input for uploading the SQL backup file to Backend for restore:

<input type="file" id="fileRestore" />

Prepare the HTML UI containers to show the progress status values:

<div class="div_progress_status">
    
    <h2>Task Status</h2>
    Api Call Index:         <span id="span_ApiCallIndex"></span><br>
    Task Id:                <span id="span_TaskId"></span><br>
    Task Type:              <span id="span_TaskType"></span><br>
    File Download Web Path: <span id="span_FileDownloadWebPath"></span><br>
    Is Completed:           <span id="span_IsCompleted"></span><br>
    Is Cancelled:           <span id="span_IsCancelled"></span><br>
    Request Cancel:         <span id="span_RequestCancel"></span><br>
    Has Error:              <span id="span_HasError"></span><br>
    Error Msg:              <span id="span_ErrorMsg"></span><br>
    
    <h2>Main Progress</h2>
    Percent Completed:      <span id="span_PercentCompleted"></span><br>
    
    <h2>Backup Progress Status</h2>
    Total Tables:           <span id="span_TotalTables"></span><br>
    Current Table Index:    <span id="span_CurrentTableIndex"></span><br>
    Current Table Name:     <span id="span_CurrentTableName"></span><br>
    Total Rows:             <span id="span_TotalRows"></span><br>
    Current Rows:           <span id="span_CurrentRows"></span><br>
    Total Rows Cur Table:   <span id="span_TotalRowsCurrentTable"></span><br>
    Current Rows Cur Table: <span id="span_CurrentRowsCurrentTable"></span><br>
    
    <h2>Restore Progress Status</h2>
    Total Bytes:            <span id="span_TotalBytes"></span><br>
    Current Bytes:          <span id="span_CurrentBytes"></span><br>
    
</div>

Write the JavaScript for the functions:

Cache the container globally:

let span_ApiCallIndex = document.getElementById("span_ApiCallIndex");
let span_TaskId = document.getElementById("span_TaskId");
let span_TaskType = document.getElementById("span_TaskType");
let span_FileDownloadWebPath = document.getElementById("span_FileDownloadWebPath");
let span_IsCompleted = document.getElementById("span_IsCompleted");
let span_IsCancelled = document.getElementById("span_IsCancelled");
let span_RequestCancel = document.getElementById("span_RequestCancel");
let span_HasError = document.getElementById("span_HasError");
let span_ErrorMsg = document.getElementById("span_ErrorMsg");
let span_PercentCompleted = document.getElementById("span_PercentCompleted");
let span_TotalTables = document.getElementById("span_TotalTables");
let span_CurrentTableIndex = document.getElementById("span_CurrentTableIndex");
let span_CurrentTableName = document.getElementById("span_CurrentTableName");
let span_TotalRows = document.getElementById("span_TotalRows");
let span_CurrentRows = document.getElementById("span_CurrentRows");
let span_TotalRowsCurrentTable = document.getElementById("span_TotalRowsCurrentTable");
let span_CurrentRowsCurrentTable = document.getElementById("span_CurrentRowsCurrentTable");
let span_TotalBytes = document.getElementById("span_TotalBytes");
let span_CurrentBytes = document.getElementById("span_CurrentBytes");

// initialize global task related variables
const urlApi = "/apiBakcup";
let currentTaskId = 0;
let api_call_index = 0;
let intervalTimer = null; // the timer
let networkErrorCount = 0;

Backup

async function startBackup() {

    try {
        
        // build the form data
        const formData = new FormData();
        formData.append("action", "start_backup");

        // send the form data to the Backend
        const result = await fetch(urlApi, {
            method: "POST",
            body: formData,
            credentials: "include"
        });

        // Backend returns the task id
        if (result.ok) {
        
            // the Backend is designed to return JSON formatted string
            // convert the string into JSON object in JavaScript
            let jsonObject = await result.json();
            
            // save the task id globally
            currentTaskId = jsonObject.TaskId;

            // Call the timer to start updating the progress status
            startProgressUpdateTimer();
        }
        else {
            let errMsg = await result.text();
            alert(errMsg);
        }
    }
    catch (err) {
        alert(err);
    }
}

Restore

async function startRestore() {
    try {

        // additional step to check the file before sending HTTP request
        if (!fileRestore.files[0]) {
            alert("Please select a file to restore");
            return;
        }
        
        // send/submit the request
        const formData = new FormData();
        formData.append("action", "start_restore");
        formData.append("file_restore", fileRestore.files[0]);

        const result = await fetch(urlApi, {
            method: "POST",
            body: formData,
            credentials: "include"
        });

        if (result.ok) {
        
            let jsonObject = await result.json();
            currentTaskId = jsonObject.TaskId;
            
            startProgressUpdateTimer();
        }
        else {
            let errMsg = await result.text();
            alert(errMsg);
        }
    }
    catch (err) {
        alert(err);
    }
}

Stop Task

async function stopTask() {
    try {
        if (!currentTaskId || currentTaskId == 0) {
            alert("No active task to stop");
            return;
        }

        const formData = new FormData();
        formData.append("action", "stop_task");
        formData.append("taskid", currentTaskId);

        const result = await fetch(urlApi, {
            method: "POST",
            body: formData,
            credentials: "include"
        });

        if (result.ok) {
            showGoodMessage("Task is Stopping", "The task is being called to stop");
        }
        else {
            let errMsg = await result.text();
            console.log(errMsg);
        }
    }
    catch (err) {
        console.log(err);
    }
}

Get Status

function startProgressUpdateTimer() {

    // reset network error counter
    networkErrorCount = 0;
    
    // stop existing timer
    stopProgressUpdateTimer();
    
    // start the interval timer here
    intervalTimer = setInterval(
        () => { getStatus() },
        250);
}

function stopProgressUpdateTimer() {

    // if the timer is active
    if (intervalTimer) {
    
        // stop and clear the timer
        clearInterval(intervalTimer);
        intervalTimer = null;
    }
}

async function getStatus() {
    try {
        // increment the api call index
        api_call_index++;

        const formData = new FormData();
        formData.append("action", "get_status");
        formData.append("taskid", currentTaskId);
        formData.append("api_call_index", api_call_index);

        const result = await fetch(urlApi, {
            method: "POST",
            body: formData,
            credentials: "include"
        });

        if (result.ok) {
        
            // the Backend is designed to return JSON formatted string
            // convert the response into JSON object
            
            let jsonObject = await result.json();

            if (jsonObject.ApiCallIndex != api_call_index) {
                // late echo, ignore
                return;
            }
            
            // the task is completed
            if (jsonObject.IsCompleted) {
                stopProgressUpdateTimer();
            }

            // pass the json object to another function for updating the UI
            updateUiValues(jsonObject);
        }
        else {
        
            // increment the error counter
            networkErrorCount++;
            let errMsg = await result.text();
            console.log(`Error: ${errMsg}`);
        }
    }
    catch (err) {
    
        // increment the error counter
        networkErrorCount++;
        mb_status.textContent += `Error: ${err}<br>`;
        console.log(`Error: ${errMsg}`);
    }

    // stop the timer if connection to Backend server failed for maximum 3 times
    if (networkErrorCount >= 3) {
        stopProgressUpdateTimer();
    }
}

Update UI With Backend Provided Progress Status

function updateUiValues(jsonObject) {

    // Task Control Properties
    span_ApiCallIndex.textContent = jsonObject.ApiCallIndex;
    span_TaskId.textContent = jsonObject.TaskId;
    span_TaskType.textContent = jsonObject.TaskType;
    span_FileDownloadWebPath.textContent = jsonObject.FileDownloadWebPath;
    span_IsCompleted.textContent = jsonObject.IsCompleted;
    span_IsCancelled.textContent = jsonObject.IsCancelled;
    span_RequestCancel.textContent = jsonObject.RequestCancel;
    span_HasError.textContent = jsonObject.HasError;
    span_ErrorMsg.textContent = jsonObject.ErrorMsg;
    
    // Main Progress
    span_PercentCompleted.textContent = jsonObject.PercentCompleted;
    
    // Backup Progress Status
    span_TotalTables.textContent = jsonObject.TotalTables;
    span_CurrentTableIndex.textContent = jsonObject.CurrentTableIndex;
    span_CurrentTableName.textContent = jsonObject.CurrentTableName;
    span_TotalRows.textContent = jsonObject.TotalRows;
    span_CurrentRows.textContent = jsonObject.CurrentRows;
    span_TotalRowsCurrentTable.textContent = jsonObject.TotalRowsCurrentTable;
    span_CurrentRowsCurrentTable.textContent = jsonObject.CurrentRowsCurrentTable;
    
    // Restore Progress Status
    span_TotalBytes.textContent = jsonObject.TotalBytes;
    span_CurrentBytes.textContent = jsonObject.CurrentBytes;
}

There we have complete the cycle and introduced the fundamental working mechanism of handling progress reporting using HTTP Request + API in Web Application, Vanilla ASP.NET Web Forms.

Happy progress reporting! Cheers!


We have some demo to share.

A comprehensive and robust implementation of above elaborated principles of doing progress reporting, we have build a comprehensive UI + Backend handling for Backup and Restore of MySQL database with MySqlBackup.NET, and we have build some awesome beautiful UI Theme:

The Backend API Demo:

Frontend UI Demo:

Complex CSS Styled Themed Frontend (7 Themes/Styles):

CSS styling will not be covered here, as that will be a very long topic. You may browse the CSS styling in the repository at:

You can view our showcase (demo) of the UI theme at Youtube: https://youtu.be/D34g7ZQC6Xo

The repository of this project demonstrates 7 CSS theme idea for the same html structure:

Light theme:

progress-report-ui-theme-light.png

Dark theme:

progress-report-ui-theme-dark.png

Cyberpunk:

progress-report-ui-theme-cyberpunk.png

Alien 1986 (movie) terminal:

progress-report-ui-theme-alien1986.png

Steampunk Victorian:

progress-report-ui-theme-steampunk-victorian.png

Solar Fire:

progress-report-ui-theme-solarfire.png

Futuristic HUD:

progress-report-ui-theme-futuristic-hud.png

Again, happy progress reporting! Cheers!

Clone this wiki locally