Skip to content

Progress Reporting with MySqlBackup.NET

adriancs edited this page Jul 20, 2025 · 28 revisions

How to Implement Progress Reporting with MySqlBackup.NET

Welcome! In this comprehensive guide, we'll walk you through creating beautiful, real-time progress reports for database backup and restore operations using MySqlBackup.NET. Whether you're building a desktop application or a web interface, this tutorial will help you create professional-looking progress indicators that keep your users informed throughout the entire process.

🎥 Live Demo

Before we dive into the code, check out this visual demonstration of what we'll be building:

MySqlBackup.NET Progress Reporting Visual Effect Demo: https://youtu.be/D34g7ZQC6Xo

This video showcases the real-time progress reporting system in action, complete with multiple beautiful CSS themes that you can customize for your application.

Understanding the Foundation

MySqlBackup.NET provides two essential classes that make progress reporting possible:

1. ExportProgressArgs.cs

This class provides detailed information during backup operations, including:

  • Current table being processed
  • Total number of tables
  • Row counts (current and total)
  • Progress percentages

2. ImportProgressArgs.cs

This class tracks restore operations with:

  • Bytes processed vs. total bytes
  • Import completion percentage
  • Real-time progress updates

Getting Started: Basic Progress Events

The magic happens when you subscribe to progress change events. Here's how to set up the fundamental progress reporting:

Backup Progress Monitoring

void Backup()
{
    using (var conn = new MySqlConnection(connectionString))
    using (var cmd = conn.CreateCommand())
    using (var mb = new MySqlBackup(cmd))
    {
        conn.Open();
        
        // Set update frequency (100ms = 10 updates per second)
        mb.ExportInfo.IntervalForProgressReport = 100;
        
        // Subscribe to progress events
        mb.ExportProgressChanged += Mb_ExportProgressChanged;
        
        mb.ExportToFile(filePathSql);
    }
}

private void Mb_ExportProgressChanged(object sender, ExportProgressArgs e)
{
    // Rich progress information available
    string currentTable = e.CurrentTableName;
    int totalTables = e.TotalTables;
    int currentTableIndex = e.CurrentTableIndex;
    long totalRows = e.TotalRowsInAllTables;
    long currentRows = e.CurrentRowIndexInAllTables;
    long currentTableRows = e.TotalRowsInCurrentTable;
    long currentTableProgress = e.CurrentRowIndexInCurrentTable;

    // Calculate completion percentage
    int percentCompleted = 0;
    if (e.CurrentRowIndexInAllTables > 0L && e.TotalRowsInAllTables > 0L)
    {
        if (e.CurrentRowIndexInAllTables >= e.TotalRowsInAllTables)
        {
            percentCompleted = 100;
        }
        else
        {
            percentCompleted = (int)(e.CurrentRowIndexInAllTables * 100L / e.TotalRowsInAllTables);
        }
    }
}

Restore Progress Monitoring

void Restore()
{
    using (var conn = config.GetNewConnection())
    using (var cmd = conn.CreateCommand())
    using (var mb = new MySqlBackup(cmd))
    {
        conn.Open();
        
        // Set update frequency
        mb.ImportInfo.IntervalForProgressReport = 100;
        
        // Subscribe to progress events
        mb.ImportProgressChanged += Mb_ImportProgressChanged;
        
        mb.ImportFromFile(filePathSql);
    }
}

private void Mb_ImportProgressChanged(object sender, ImportProgressArgs e)
{
    // Byte-based progress tracking
    long totalBytes = e.TotalBytes;
    long currentBytes = e.CurrentBytes;
    
    // Calculate completion percentage
    int percentCompleted = 0;
    if (e.CurrentBytes > 0L && e.TotalBytes > 0L)
    {
        if (e.CurrentBytes >= e.TotalBytes)
        {
            percentCompleted = 100;
        }
        else
        {
            percentCompleted = (int)(e.CurrentBytes * 100L / e.TotalBytes);
        }
    }
}

Choosing the Right Update Frequency

The update interval significantly impacts your application's responsiveness:

// Configure update frequency based on your needs:

// Desktop/Local Applications (low latency)
mb.ExportInfo.IntervalForProgressReport = 100;  // 10 updates/second
mb.ImportInfo.IntervalForProgressReport = 250;  // 4 updates/second

// Web Applications (higher latency)
mb.ExportInfo.IntervalForProgressReport = 500;  // 2 updates/second
mb.ImportInfo.IntervalForProgressReport = 1000; // 1 update/second

Performance Guidelines:

  • 100ms: Perfect for desktop applications - very responsive
  • 250ms: Great balance for most applications
  • 500ms: Ideal for web applications with moderate traffic
  • 1000ms: Best for high-traffic web applications

Architecture: The Two-Thread Pattern

Understanding the architecture is crucial for building robust progress reporting:

MySqlBackup.NET Internal Threads

  1. Main Process Thread: Executes actual backup/restore operations
  2. Progress Reporting Thread: Periodically reports progress via timer events

Your Application Threads

  1. Backend Thread: Handles MySqlBackup.NET operations and caches progress data
  2. UI Thread: Retrieves cached data and updates the user interface

Creating the Progress Data Model

Here's a comprehensive class to cache all progress information:

class ProgressReportTask
{
    public int ApiCallIndex { get; set; }
    public int TaskId { get; set; }
    public int TaskType { get; set; }  // 1 = backup, 2 = restore
    public string FileName { get; set; }
    public string SHA256 { get; set; }

    // Task status tracking
    public bool IsStarted { get; set; }
    public bool IsCompleted { get; set; } = false;
    public bool IsCancelled { get; set; } = false;
    public bool RequestCancel { get; set; } = false;
    public bool HasError { get; set; } = false;
    public string ErrorMsg { get; set; } = "";
    
    // Time tracking
    public DateTime TimeStart { get; set; } = DateTime.MinValue;
    public DateTime TimeEnd { get; set; } = DateTime.MinValue;
    public TimeSpan TimeUsed { get; set; } = TimeSpan.Zero;

    // Backup-specific progress data
    public int TotalTables { get; set; } = 0;
    public int CurrentTableIndex { get; set; } = 0;
    public string CurrentTableName { get; set; } = "";
    public long TotalRowsCurrentTable { get; set; } = 0;
    public long CurrentRowCurrentTable { get; set; } = 0;
    public long TotalRows { get; set; } = 0;
    public long CurrentRowIndex { get; set; } = 0;

    // Restore-specific progress data
    public long TotalBytes { get; set; } = 0;
    public long CurrentBytes { get; set; } = 0;

    public int PercentCompleted { get; set; } = 0;

    // UI-friendly properties
    [JsonPropertyName("TaskTypeName")]
    public string TaskTypeName
    {
        get
        {
            return TaskType switch
            {
                1 => "Backup",
                2 => "Restore",
                _ => "Unknown"
            };
        }
    }

    [JsonPropertyName("TimeStartDisplay")]
    public string TimeStartDisplay => TimeStart.ToString("yyyy-MM-dd HH:mm:ss");

    [JsonPropertyName("TimeEndDisplay")]
    public string TimeEndDisplay => TimeEnd.ToString("yyyy-MM-dd HH:mm:ss");

    [JsonPropertyName("TimeUsedDisplay")]
    public string TimeUsedDisplay => TimeDisplayHelper.TimeSpanToString(TimeUsed);

    [JsonPropertyName("FileDownloadUrl")]
    public string FileDownloadUrl
    {
        get
        {
            if (!string.IsNullOrEmpty(FileName))
            {
                return $"/apiFiles?folder=backup&filename={FileName}";
            }
            return "";
        }
    }
}

Building the Backend API

Let's create a powerful ASP.NET Web Forms API to handle all operations. First, create a new page and keep only the page directive:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiProgressReport.aspx.cs" Inherits="System.pages.apiProgressReport" %>

Route this page to /apiProgressReport using your preferred routing method.

Main API Controller

// Thread-safe progress cache
static ConcurrentDictionary<int, ProgressReportTask> dicTask = new ConcurrentDictionary<int, ProgressReportTask>();

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsUserAuthorized())
    {
        Response.StatusCode = 401;
        Response.Write("0|Unauthorized access");
        Response.End();
        return;
    }

    string action = (Request["action"] + "").ToLower();

    switch (action)
    {
        case "backup":
            Backup();
            break;
        case "restore":
            Restore();
            break;
        case "stoptask":
            StopTask();
            break;
        case "gettaskstatus":
            GetTaskStatus();
            break;
        default:
            Response.StatusCode = 400;
            Response.Write("0|Invalid action");
            break;
    }
}

bool IsUserAuthorized()
{
    // Implement your authentication logic here
    // Check user login and backup permissions
    return true; // Simplified for demo
}

With this setup, the front end can access the api in 2 main ways

Example 1: by using query string, simple direct:

/apiProgressReport?action=backup

/apiProgressReport?action=restore

/apiProgressReport?action=stoptask

/apiProgressReport?action=gettaskstatus

Example 2: by using post request through fetchapi or xmlhttprequest in a nutshell, the javascript fetchapi will look something like this:

async function backup() {

    const formData = new FormData();
    formData.append('action', 'backup');

    try {

        const response = await fetch('/apiProgressReport', {
            method: 'POST',
            body: formData,
            credentials: 'include' // submit user login credential
        });

        if (response.ok) {
            
            // backend server successfully executed the task
            
        }
        else {
            
            // server error
        }
    }
    catch (err) {
        
        // network error
        
    }
}

async function restore() {

    const formData = new FormData();
    formData.append('action', 'restore');
    // attach and send the file to server
    formData.append('fileRestore', fileRestore.files[0]); 

    try {

        const response = await fetch('/apiProgressReport', {
            method: 'POST',
            body: formData,
            credentials: 'include' // submit user login credential
        });

        if (response.ok) {
            
            // backend server successfully executed the task
            
        }
        else {
            
            // server error
        }
    }
    catch (err) {
        
        // network error
        
    }
}

Backup Implementation

void Backup()
{
    var taskId = GetNewTaskId();
    
    // Start task asynchronously (fire and forget pattern)
    _ = Task.Run(() => { BeginExport(taskId); });
    
    // Immediately return task ID to frontend
    Response.Write(taskId.ToString());
}

int thisTaskId = 0;

void BeginExport(int newTaskId)
{
    thisTaskId = newTaskId;

    ProgressReportTask task = new ProgressReportTask()
    {
        TaskId = newTaskId,
        TaskType = 1, // backup
        TimeStart = DateTime.Now,
        IsStarted = true
    };

    dicTask[newTaskId] = task;

    try
    {
        string folder = Server.MapPath("~/App_Data/backup");
        Directory.CreateDirectory(folder);

        string fileName = $"backup-{DateTime.Now:yyyy-MM-dd_HHmmss}.sql";
        string filePath = Path.Combine(folder, fileName);

        using (var conn = config.GetNewConnection())
        using (var cmd = conn.CreateCommand())
        using (var mb = new MySqlBackup(cmd))
        {
            conn.Open();
            mb.ExportInfo.IntervalForProgressReport = 100;
            mb.ExportProgressChanged += Mb_ExportProgressChanged;
            mb.ExportToFile(filePath);
        }
        
        // Handle cancellation or completion
        if (task.RequestCancel)
        {
            task.IsCancelled = true;
            try
            {
                if (File.Exists(filePath))
                    File.Delete(filePath);
            }
            catch { }
        }
        else
        {
            // Successfully completed
            task.FileName = fileName;
            task.SHA256 = Sha256.Compute(filePath);
        }
        
        task.TimeEnd = DateTime.Now;
        task.TimeUsed = DateTime.Now - task.TimeStart;
        task.IsCompleted = true;
    }
    catch (Exception ex)
    {
        task.HasError = true;
        task.ErrorMsg = ex.Message;
        task.TimeEnd = DateTime.Now;
        task.IsCompleted = true;
    }
}

private void Mb_ExportProgressChanged(object sender, ExportProgressArgs e)
{
    if (dicTask.TryGetValue(thisTaskId, out var task))
    {
        // Update all progress information
        task.CurrentTableName = e.CurrentTableName;
        task.TotalTables = e.TotalTables;
        task.CurrentTableIndex = e.CurrentTableIndex;
        task.TotalRows = e.TotalRowsInAllTables;
        task.CurrentRowIndex = e.CurrentRowIndexInAllTables;
        task.TotalRowsCurrentTable = e.TotalRowsInCurrentTable;
        task.CurrentRowCurrentTable = e.CurrentRowIndexInCurrentTable;

        // Calculate percentage
        if (e.CurrentRowIndexInAllTables > 0L && e.TotalRowsInAllTables > 0L)
        {
            if (e.CurrentRowIndexInAllTables >= e.TotalRowsInAllTables)
            {
                task.PercentCompleted = 100;
            }
            else
            {
                task.PercentCompleted = (int)(e.CurrentRowIndexInAllTables * 100L / e.TotalRowsInAllTables);
            }
        }
        else
        {
            task.PercentCompleted = 0;
        }

        // Handle cancellation requests
        if (task.RequestCancel)
        {
            ((MySqlBackup)sender).StopAllProcess();
        }
    }
}

Restore Implementation

void Restore()
{
    var taskId = GetNewTaskId();

    ProgressReportTask task = new ProgressReportTask()
    {
        TaskId = taskId,
        TaskType = 2, // restore
        TimeStart = DateTime.Now,
        IsStarted = true
    };

    dicTask[taskId] = task;

    // Validate uploaded file
    if (Request.Files.Count == 0)
    {
        task.IsCompleted = true;
        task.HasError = true;
        task.ErrorMsg = "No file uploaded";
        task.TimeEnd = DateTime.Now;
        return;
    }

    string fileExtension = Request.Files[0].FileName.ToLower().Trim();
    if (!fileExtension.EndsWith(".zip") && !fileExtension.EndsWith(".sql"))
    {
        task.IsCompleted = true;
        task.HasError = true;
        task.ErrorMsg = "Invalid file type. Only .zip or .sql files are supported.";
        task.TimeEnd = DateTime.Now;
        return;
    }

    // Save uploaded file
    string folder = Server.MapPath("~/App_Data/backup");
    Directory.CreateDirectory(folder);
    string fileName = $"restore-{DateTime.Now:yyyy-MM-dd_HHmmss}";
    string filePath = Path.Combine(folder, fileName + ".sql");

    if (fileExtension.EndsWith(".zip"))
    {
        string zipPath = Path.Combine(folder, fileName + ".zip");
        Request.Files[0].SaveAs(zipPath);
        ZipHelper.ExtractFile(zipPath, filePath);
        task.FileName = fileName + ".zip";
    }
    else
    {
        Request.Files[0].SaveAs(filePath);
        task.FileName = fileName + ".sql";
    }

    // Start restore process asynchronously
    _ = Task.Run(() => { BeginRestore(taskId, filePath); });
    
    Response.Write(taskId.ToString());
}

void BeginRestore(int newTaskId, string filePath)
{
    thisTaskId = newTaskId;

    if (dicTask.TryGetValue(thisTaskId, out ProgressReportTask task))
    {
        try
        {
            task.FileName = Path.GetFileName(filePath);
            task.SHA256 = Sha256.Compute(filePath);

            using (var conn = config.GetNewConnection())
            using (var cmd = conn.CreateCommand())
            using (var mb = new MySqlBackup(cmd))
            {
                conn.Open();
                mb.ImportInfo.IntervalForProgressReport = 100;
                mb.ImportProgressChanged += Mb_ImportProgressChanged;
                mb.ImportFromFile(filePath);
            }
            
            if (task.RequestCancel)
            {
                task.IsCancelled = true;
            }
            
            task.TimeEnd = DateTime.Now;
            task.TimeUsed = DateTime.Now - task.TimeStart;
            task.IsCompleted = true;
        }
        catch (Exception ex)
        {
            task.HasError = true;
            task.ErrorMsg = ex.Message;
            task.TimeEnd = DateTime.Now;
            task.TimeUsed = DateTime.Now - task.TimeStart;
            task.IsCompleted = true;
        }
    }
}

private void Mb_ImportProgressChanged(object sender, ImportProgressArgs e)
{
    if (dicTask.TryGetValue(thisTaskId, out var task))
    {
        task.TotalBytes = e.TotalBytes;
        task.CurrentBytes = e.CurrentBytes;

        // Calculate percentage
        if (e.CurrentBytes > 0L && e.TotalBytes > 0L)
        {
            if (e.CurrentBytes >= e.TotalBytes)
            {
                task.PercentCompleted = 100;
            }
            else
            {
                task.PercentCompleted = (int)(e.CurrentBytes * 100L / e.TotalBytes);
            }
        }
        else
        {
            task.PercentCompleted = 0;
        }

        // Handle cancellation requests
        if (task.RequestCancel)
        {
            ((MySqlBackup)sender).StopAllProcess();
        }
    }
}

Task Control Methods

void StopTask()
{
    if (int.TryParse(Request["taskid"] + "", out int taskId))
    {
        if (dicTask.TryGetValue(taskId, out ProgressReportTask task))
        {
            task.RequestCancel = true;
            Response.Write("1");
        }
        else
        {
            Response.Write("0|Task not found");
        }
    }
    else
    {
        Response.Write("0|Invalid task ID");
    }
}

void GetTaskStatus()
{
    if (int.TryParse(Request["apicallid"] + "", out int apiCallId))
    {
        if (int.TryParse(Request["taskid"] + "", out int taskId))
        {
            if (dicTask.TryGetValue(taskId, out ProgressReportTask task))
            {
                task.ApiCallIndex = apiCallId;

                string json = JsonSerializer.Serialize(task);
                Response.Clear();
                Response.ContentType = "application/json";
                Response.Write(json);
            }
        }
    }
}

Handling Late Echo Responses

The apiCallId system prevents UI corruption from network latency issues:

Normal scenario:

Call 1 → Response 1: 5%
Call 2 → Response 2: 10%
Call 3 → Response 3: 15%

Latency scenario:

Call 1 → (delayed)
Call 2 → Response 2: 10%
Call 3 → Response 3: 15%
        ↓
Response 1: 5% (ignored due to old apiCallId)

Building the Frontend

Basic Value Container

You can use <span> as the container for the values:

// data that is manually handled by user
<span id="labelTaskId"></span>
<span id="labelPercent">0</span>
<span id="lableTimeStart"></span>
<span id="lableTimeEnd"></span>
<span id="lableTimeElapse"></span>
<span id="labelSqlFilename"></span> // the download webpath for the generated sql dump file
<span id="labelSha256"></span> // the SHA 256 checksum for the generate file

// typical status: running, completed, error, cancelled
<span id="labelTaskStatus"></span>

// the following data fields provided by mysqlbackup.net during the progress change events:

// fields for backup used
<span id="labelCurTableName"></span>
<span id="labelCurTableIndex"></span>
<span id="labelTotalTables"></span>
<span id="labelCurrentRowsAllTables"></span>
<span id="labelTotalRowsAllTable">0</span>
<span id="labelCurrentRowsCurrentTables"></span>
<span id="labelTotalRowsCurrentTable">0</span>

// fields for restore used
<span id="labelCurrentBytes"></span>
<span id="lableTotalBytes"></span>

HTML Structure

Example of a comprehensive progress display:

<!-- Progress Bar -->
<div id="progress_bar_container">
    <div id="progress_bar_indicator">
        <span id="labelPercent">0</span> %
    </div>
</div>

<!-- Control Buttons -->
<div class="controls">
    <button type="button" onclick="backup();">Backup Database</button>
    <button type="button" onclick="restore();">Restore Database</button>
    <button type="button" onclick="stopTask();">Stop Current Task</button>
    
    <label for="fileRestore">Select Restore File:</label>
    <input type="file" id="fileRestore" accept=".sql,.zip" />
</div>

<!-- Detailed Status Display -->
<div class="task-status">
    <table>
        <tr>
            <td>Task ID</td>
            <td><span id="labelTaskId">--</span></td>
        </tr>
        <tr>
            <td>Status</td>
            <td>
                <span id="labelTaskStatus">Ready</span>
                <span id="labelTaskMessage"></span>
            </td>
        </tr>
        <tr>
            <td>Time</td>
            <td>
                Start: <span id="labelTimeStart">--</span> |
                End: <span id="labelTimeEnd">--</span> |
                Duration: <span id="labelTimeElapsed">--</span>
            </td>
        </tr>
        <tr>
            <td>File</td>
            <td>
                <span id="labelSqlFilename">--</span><br>
                SHA256: <span id="labelSha256">--</span>
            </td>
        </tr>
        
        <!-- Backup-specific fields -->
        <tr class="backup-only">
            <td>Current Table</td>
            <td>
                <span id="labelCurrentTableName">--</span>
                (<span id="labelCurrentTableIndex">--</span> / <span id="labelTotalTables">--</span>)
            </td>
        </tr>
        <tr class="backup-only">
            <td>All Tables Progress</td>
            <td>
                <span id="labelCurrentRowsAllTables">--</span> / <span id="labelTotalRowsAllTables">--</span>
            </td>
        </tr>
        <tr class="backup-only">
            <td>Current Table Progress</td>
            <td>
                <span id="labelCurrentRowsCurrentTable">--</span> / <span id="labelTotalRowsCurrentTable">--</span>
            </td>
        </tr>
        
        <!-- Restore-specific fields -->
        <tr class="restore-only">
            <td>Progress</td>
            <td>
                <span id="labelCurrentBytes">--</span> / <span id="labelTotalBytes">--</span> bytes
            </td>
        </tr>
    </table>
</div>

JavaScript Implementation

Initialization and Variables

// Global variables
let taskId = 0;
let apiCallId = 0;
let intervalTimer = null;
let intervalMs = 1000;

// Cache DOM elements for better performance
const elements = {
    fileRestore: document.querySelector("#fileRestore"),
    progressBar: document.querySelector("#progress_bar_indicator"),
    labelPercent: document.querySelector("#labelPercent"),
    labelTaskId: document.querySelector("#labelTaskId"),
    labelTimeStart: document.querySelector("#labelTimeStart"),
    labelTimeEnd: document.querySelector("#labelTimeEnd"),
    labelTimeElapsed: document.querySelector("#labelTimeElapsed"),
    labelTaskStatus: document.querySelector("#labelTaskStatus"),
    labelTaskMessage: document.querySelector("#labelTaskMessage"),
    labelSqlFilename: document.querySelector("#labelSqlFilename"),
    labelSha256: document.querySelector("#labelSha256"),
    
    // Backup-specific elements
    labelCurrentTableName: document.querySelector("#labelCurrentTableName"),
    labelCurrentTableIndex: document.querySelector("#labelCurrentTableIndex"),
    labelTotalTables: document.querySelector("#labelTotalTables"),
    labelCurrentRowsAllTables: document.querySelector("#labelCurrentRowsAllTables"),
    labelTotalRowsAllTables: document.querySelector("#labelTotalRowsAllTables"),
    labelCurrentRowsCurrentTable: document.querySelector("#labelCurrentRowsCurrentTable"),
    labelTotalRowsCurrentTable: document.querySelector("#labelTotalRowsCurrentTable"),
    
    // Restore-specific elements
    labelCurrentBytes: document.querySelector("#labelCurrentBytes"),
    labelTotalBytes: document.querySelector("#labelTotalBytes")
};

Core Functions

Backup, Restore, Stop

async function backup() {
    resetUIValues();

    const formData = new FormData();
    formData.append('action', 'backup');

    try {
        const response = await fetch('/apiProgressReport', {
            method: 'POST',
            body: formData,
            credentials: 'include'
        });

        if (response.ok) {
            const responseText = await response.text();
            
            if (responseText.startsWith("0|")) {
                const error = responseText.substring(2);
                showErrorMessage("Backup Failed", error);
            } else {
                taskId = parseInt(responseText);
                
                if (!isNaN(taskId)) {
                    intervalMs = 1000;
                    startIntervalTimer();
                    showSuccessMessage("Backup Started", "Database backup has begun successfully");
                } else {
                    showErrorMessage("Error", `Invalid task ID: ${responseText}`);
                }
            }
        } else {
            const errorText = await response.text();
            showErrorMessage("Server Error", errorText);
        }
    } catch (error) {
        console.error('Backup API call failed:', error);
        showErrorMessage("Network Error", error.message);
        stopIntervalTimer();
    }
}

async function restore() {
    resetUIValues();

    if (!elements.fileRestore.files || elements.fileRestore.files.length === 0) {
        showErrorMessage("No File Selected", "Please select a backup file to restore");
        return;
    }

    const formData = new FormData();
    formData.append('action', 'restore');
    formData.append('fileRestore', elements.fileRestore.files[0]);

    try {
        const response = await fetch('/apiProgressReport', {
            method: 'POST',
            body: formData,
            credentials: 'include'
        });

        if (response.ok) {
            const responseText = await response.text();
            
            if (responseText.startsWith("0|")) {
                const error = responseText.substring(2);
                showErrorMessage("Restore Failed", error);
            } else {
                taskId = parseInt(responseText);
                
                if (!isNaN(taskId)) {
                    intervalMs = 1000;
                    startIntervalTimer();
                    showSuccessMessage("Restore Started", "Database restore has begun successfully");
                } else {
                    showErrorMessage("Error", `Invalid task ID: ${responseText}`);
                }
            }
        } else {
            const errorText = await response.text();
            showErrorMessage("Server Error", errorText);
        }
    } catch (error) {
        console.error('Restore API call failed:', error);
        showErrorMessage("Network Error", error.message);
        stopIntervalTimer();
    }
}

async function stopTask() {
    if (!taskId || taskId === 0) {
        showErrorMessage("No Active Task", "There is no running task to stop");
        return;
    }

    const formData = new FormData();
    formData.append("action", "stoptask");
    formData.append("taskid", taskId);

    try {
        const response = await fetch('/apiProgressReport', {
            method: 'POST',
            body: formData,
            credentials: 'include'
        });

        if (response.ok) {
            const responseText = await response.text();
            
            if (responseText === "1") {
                showSuccessMessage("Stop Requested", "The task is being cancelled...");
            } else {
                const error = responseText.startsWith("0|") ? responseText.substring(2) : responseText;
                showErrorMessage("Stop Failed", error);
            }
        } else {
            const errorText = await response.text();
            showErrorMessage("Server Error", errorText);
        }
    } catch (error) {
        console.error('Stop task API call failed:', error);
        showErrorMessage("Network Error", error.message);
        stopIntervalTimer();
    }
}

Progress Monitoring

async function fetchTaskStatus() {
    apiCallId++;

    const formData = new FormData();
    formData.append('action', 'gettaskstatus');
    formData.append('taskid', taskId);
    formData.append('apicallid', apiCallId);

    try {
        const response = await fetch('/apiProgressReport', {
            method: 'POST',
            body: formData,
            credentials: 'include'
        });

        if (response.ok) {
            const responseText = await response.text();
            
            try {
                const jsonObject = JSON.parse(responseText);

                // Ignore late responses
                if (jsonObject.ApiCallIndex !== apiCallId) {
                    return;
                }

                updateUIValues(jsonObject);
            } catch (parseError) {
                console.error('JSON parsing error:', parseError);
                showErrorMessage("Data Error", "Invalid response from server");
            }
        } else {
            const errorText = await response.text();
            showErrorMessage("Server Error", errorText);
        }
    } catch (error) {
        console.error('Status fetch failed:', error);
        showErrorMessage("Network Error", error.message);
        stopIntervalTimer();
    }
}

function updateUIValues(data) {
    // Optimize update frequency when task starts
    if (data.PercentCompleted > 0 && intervalMs === 1000) {

        // change the timer interval time
        intervalMs = 100;
        stopIntervalTimer();
        setTimeout(startIntervalTimer, 500);
    }

    // Stop monitoring when task completes
    if (data.IsCompleted || data.HasError || data.IsCancelled) {
        stopIntervalTimer();
    }

    // Update basic information
    elements.labelTaskId.textContent = data.TaskId || "--";
    elements.labelTimeStart.textContent = data.TimeStartDisplay || "--";
    elements.labelTimeEnd.textContent = data.TimeEndDisplay || "--";
    elements.labelTimeElapsed.textContent = data.TimeUsedDisplay || "--";

    // Update progress bar
    const percent = data.PercentCompleted || 0;
    elements.labelPercent.style.display = "block";
    elements.labelPercent.textContent = percent;
    elements.progressBar.style.width = percent + '%';

    // Update status with visual indicators
    const statusContainer = elements.labelTaskStatus.closest('td');
    
    if (data.HasError) {
        elements.labelTaskStatus.textContent = "Error";
        elements.labelTaskMessage.textContent = data.ErrorMsg || "";
        statusContainer.className = "status-error";
        showErrorMessage("Task Failed", data.ErrorMsg || "Unknown error occurred");
    } else if (data.IsCancelled) {
        elements.labelTaskStatus.textContent = "Cancelled";
        elements.labelTaskMessage.textContent = "";
        statusContainer.className = "status-cancelled";
        showWarningMessage("Task Cancelled", "The operation was cancelled by user request");
    } else if (data.IsCompleted) {
        elements.labelTaskStatus.textContent = "Completed";
        elements.labelTaskMessage.textContent = "";
        statusContainer.className = "status-complete";
        showSuccessMessage("Task Completed", "Operation finished successfully!");
    } else {
        elements.labelTaskStatus.textContent = "Running";
        elements.labelTaskMessage.textContent = "";
        statusContainer.className = "status-running";
    }

    // Update file information
    if (data.FileName && data.FileName.length > 0) {
        elements.labelSqlFilename.innerHTML = 
            `File: <a href='${data.FileDownloadUrl}' class='download-link' target='_blank'>Download ${data.FileName}</a>`;
    } else {
        elements.labelSqlFilename.textContent = data.FileName || "--";
    }
    elements.labelSha256.textContent = data.SHA256 || "--";

    // Update backup-specific information
    if (data.TaskType === 1) {
        elements.labelCurrentTableName.textContent = data.CurrentTableName || "--";
        elements.labelCurrentTableIndex.textContent = data.CurrentTableIndex || "--";
        elements.labelTotalTables.textContent = data.TotalTables || "--";
        elements.labelCurrentRowsAllTables.textContent = data.CurrentRowIndex || "--";
        elements.labelTotalRowsAllTables.textContent = data.TotalRows || "--";
        elements.labelCurrentRowsCurrentTable.textContent = data.CurrentRowCurrentTable || "--";
        elements.labelTotalRowsCurrentTable.textContent = data.TotalRowsCurrentTable || "--";

        // Hide restore-specific fields
        elements.labelCurrentBytes.textContent = "--";
        elements.labelTotalBytes.textContent = "--";
    }

    // Update restore-specific information
    if (data.TaskType === 2) {
        elements.labelCurrentBytes.textContent = formatBytes(data.CurrentBytes) || "--";
        elements.labelTotalBytes.textContent = formatBytes(data.TotalBytes) || "--";

        // Hide backup-specific fields
        elements.labelCurrentTableName.textContent = "--";
        elements.labelCurrentTableIndex.textContent = "--";
        elements.labelTotalTables.textContent = "--";
        elements.labelCurrentRowsAllTables.textContent = "--";
        elements.labelTotalRowsAllTables.textContent = "--";
        elements.labelCurrentRowsCurrentTable.textContent = "--";
        elements.labelTotalRowsCurrentTable.textContent = "--";
    }
}

Utility Functions

function resetUIValues() {
    // Reset all display elements
    Object.values(elements).forEach(element => {
        if (element && element.textContent !== undefined) {
            element.textContent = "--";
        }
    });

    // Reset progress bar
    elements.progressBar.style.width = '0%';
    elements.labelPercent.style.display = "none";
    elements.labelPercent.textContent = "0";

    // Reset status styling
    const statusContainer = elements.labelTaskStatus.closest('td');
    if (statusContainer) {
        statusContainer.className = "";
    }
}

function startIntervalTimer() {
    stopIntervalTimer();
    intervalTimer = setInterval(fetchTaskStatus, intervalMs);
}

function stopIntervalTimer() {
    if (intervalTimer) {
        clearInterval(intervalTimer);
        intervalTimer = null;
    }
}

function formatBytes(bytes) {
    if (!bytes || bytes === 0) return '0 Bytes';
    
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(1024));
    
    return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}

// Message display functions (implement based on your UI framework)
function showSuccessMessage(title, message) {
    console.log(`✅ ${title}: ${message}`);
    // Implement your success notification here
}

function showErrorMessage(title, message) {
    console.error(`❌ ${title}: ${message}`);
    // Implement your error notification here
}

function showWarningMessage(title, message) {
    console.warn(`⚠️ ${title}: ${message}`);
    // Implement your warning notification here
}

Styling Your Progress Interface

CSS styling will not be covered here, as that will be a very long topic. But however, we have already prepared 7 theme style demo in the repository. You can get it at:

The repository demonstrates 7 CSS theme idea for your reference:

  • Light theme
  • Dark theme
  • Cyberpunk theme
  • Alien 1986 (movie0 terminal theme
  • Steampunk Victorian theme
  • Solar Fire theme
  • Futuristic HUD theme

You can view our demo at Youtube: https://youtu.be/D34g7ZQC6Xo

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

Conclusion

Congratulations! You've now built a comprehensive, professional-grade progress reporting system for MySqlBackup.NET operations. This system provides:

Real-time progress updates with detailed information
Responsive, beautiful UI with multiple theme options
Robust error handling and recovery mechanisms
Scalable architecture suitable for production use
Professional user experience with smooth animations

Key Takeaways

  1. Two-thread architecture is essential for responsive progress reporting
  2. Proper error handling prevents UI corruption and improves user experience
  3. Optimized update frequencies balance responsiveness with performance
  4. Late echo protection ensures data consistency in high-latency environments
  5. Beautiful themes create engaging user experiences

The techniques demonstrated here can be easily adapted to other frameworks like WinForms, WPF, .NET Core, or any other platform where you need to implement progress reporting.

Next Steps

  • Explore the video demonstration to see all themes in action
  • Customize the themes to match your application's branding
  • Implement additional features like progress history or scheduled backups
  • Consider extending the system to support multiple concurrent operations

Happy coding, and may your database operations always complete successfully! 🚀


For more advanced features and updates, visit the [MySqlBackup.NET GitHub repository](https://github.com/MySqlBackupNET/MySqlBackup.Net).

Clone this wiki locally