-
Notifications
You must be signed in to change notification settings - Fork 109
Progress Reporting in Web Application using HTTP Request API Endpoint
Main Content:
- Part 1: Introduction of Progress Reporting with MySqlBackup.NET in WinForms
- Part 2: Progress Reporting in Web Application using HTTP Request/API Endpoint
- Part 3: Progress Reporting in Web Application using Web Socket/API Endpoint
- Part 4: Progress Reporting in Web Application using Server-Sent Events (SSE)
- Part 5: Building a Portable JavaScript Object for MySqlBackup.NET Progress Reporting Widget
- (old doc) Progress Reporting with MySqlBackup.NET
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
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.
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:
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:
- Simple UI - ProgressReport2.aspx
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 - light.css
- Dark theme - dark.css
- Cyberpunk theme - cyberpunk.css
- Alien 1986 (movie) terminal theme - retro.css
- Steampunk Victorian theme - steampunk.css
- Solar Fire theme - solarfire.css
- Futuristic HUD theme - hud.css
Light theme:
Dark theme:
Cyberpunk:
Alien 1986 (movie) terminal:
Steampunk Victorian:
Solar Fire:
Futuristic HUD:
Again, happy progress reporting! Cheers!