-
Notifications
You must be signed in to change notification settings - Fork 109
Introduction of Progress Reporting with MySqlBackup.NET in WinForms
Main Content:
- Part 1-1: Introduction of Progress Reporting with MySqlBackup.NET in WinForms
- Part 1-2: Detailed WinForms Walkthrough - Progress Reporting with MySqlBackup.NET
- 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 article begins the series of doing progressing reporting with MySqlBackup.NET by introducing the basic walkthrough, concepts and core working mechanism.
The core concept is, there is a intermediary progress status caching layer between MySqlBackup.NET and the UI application.
An overview of a backup process in web application:
After you have understood the pattern and core mechanism of the progress reporting, these patterns can easily be replicated to any other forms of application, such as WinForms/WPF/Console, ASP.NET Web Forms, ASP.NET MVC, .NET Core Web App, etc.
In this series, we will be documenting the making of progress reporting in various application environment with various method:
Application | Method | Initiation of Communication Direction |
Frontend to Backend |
Backend to Frontend |
---|---|---|---|---|
Desktop Application (WinForms/WPF/Console) (Part 1: This article) |
Multi-Threading | Bi-Direction | Yes | Yes |
Web Application (Part 2) | HTTP Request | One Way | Yes (Active Initiates) |
Yes (Passive Response) |
Web Application (Part 3) | Web Socket | Bi-Direction | Yes | Yes |
Web Application (Part 4) | Server Sent Event (SSE) | One Way | No | Yes |
In desktop application, the concept of frontend and backend is a little obscure, as it is not as obvious as web application where there is a distinct identification of 2 separate layers of operations and presentations.
For the web application development, we'll be using Vanilla ASP.NET Web Forms as the UI Application to demonstrate the concept and idea of performing progress reporting with MySqlBackup.NET.
Vanilla ASP.NET Web Forms, refers to an architecture using pure HTML, JavaScript and CSS. (Zero ViewState, No Server Control, No User Control, No UpdatePanel)
Now, let us begin with the introduction of the fundamental mechanism of the process.
MySqlBackup.NET provides the following progress values during the process:
Backup/Export
- Total Rows
- Current Rows (Current Processing)
- Total Tables
- Current Tables (Current Processing)
- Current Table Name
- Total Rows in Current Table
- Current Rows in Current Table
Restore/Import
- Total Bytes
- Current Bytes
These values can be accessed/obtained through event subscriptions of
-
ExportProgressChanged
(For Backup/Export) -
ImportProgressChanged
(For Restore/Import)
Example:
Backup/Export
void Backup()
{
// optional, used for tracing task history
int taskId = GetNewTaskId();
string sqlFile = Server.MapPath("~/backup.sql");
using (var conn = config.GetNewConnection())
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
// 200ms - report progress status 5 times per second
mb.ExportInfo.IntervalForProgressReport = 200;
// subscribe the event (without tracing task id)
mb.ExportProgressChanged += Mb_ExportProgressChanged;
// subscribe the event with tracing the task id
mb.ExportProgressChanged += (sender, e) => Mb_ExportProgressChanged(sender, e, taskId);
mb.ExportToFile(sqlFile);
}
}
void Mb_ExportProgressChanged(object sender, ExportProgressArgs e)
{
string CurrentTableName = e.CurrentTableName;
long TotalRowsInCurrentTable = e.TotalRowsInCurrentTable;
long TotalRowsInAllTables = e.TotalRowsInAllTables;
long CurrentRowIndexInCurrentTable = e.CurrentRowIndexInCurrentTable;
long CurrentRowIndexInAllTables = e.CurrentRowIndexInAllTables;
int TotalTables = e.TotalTables;
int CurrentTableIndex = e.CurrentTableIndex;
// Percentage completed, this value requires manual calculation
int PercentCompleted = CalculatePercent(TotalRowsInAllTables, CurrentRowIndexInAllTables);
}
void Mb_ExportProgressChanged(object sender, ExportProgressArgs e, int taskId)
{
...
}
Restore/Import
void Restore()
{
// optional, used for tracing task history
int taskId = GetNewTaskId();
string sqlFile = Server.MapPath("~/backup.sql");
using (var conn = config.GetNewConnection())
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
// for better real-time accuracy
mb.ImportInfo.EnableParallelProcessing = false;
// 200ms - report progress status 5 times per second
mb.ImportInfo.IntervalForProgressReport = 200;
// subscribe the event (without tracing task id)
mb.ImportProgressChanged += Mb_ImportProgressChanged;
// subscribe the event with tracing the task id
mb.ImportProgressChanged += (sender, e) => Mb_ImportProgressChanged(sender, e, taskId);
mb.ImportFromFile(filePathSql);
}
}
void Mb_ImportProgressChanged(object sender, ExportProgressArgs e)
{
long TotalBytes = e.TotalBytes;
long CurrentBytes = e.CurrentBytes;
// Percentage completed, this value requires manual calculation
int PercentCompleted = CalculatePercent(TotalRowsInAllTables, CurrentRowIndexInAllTables);
}
void Mb_ImportProgressChanged(object sender, ExportProgressArgs e, int taskId)
{
...
}
If you are doing a simple one time progress report in desktop application (WinForms/WPF/Console), (more of this at the following), most probably you don't need task id tracing. Task ID is used by your application internally to trace and keep the history record of each backup or restore task.
Task Info (associates with task id) can be stored in another MySQL database or in an external 3rd party storage medium, such as SQLite.
Now, let's us move the next chapter, by looking at the progress reporting in action in Desktop Application: WinForms.
As mentioned earlier, in between MySqlBackup.NET and your application UI, there is a layer or section of "Intermediary" progress status caching, refers as "The Control Centre".
MySqlBackup.NET will send progress status to the "Control Centre", then your application UI processing logic will retrieve values from it.
In desktop application (WinForms/WPF/Console), due to the nature of the app environment, the intermediary caching can be as simple as just cache it as the class level global variable.
Enabling multi-threading:
using System.Threading.Tasks;
The code behind of the Form:
public partial class Form1 : Form
{
// MySql Connection String
string constr = "server=localhost;user=root;pwd=pwd;database=db;convertzerodatetime=true;";
// The global cached values (class level)
string CurrentTableName = "";
long TotalRowsInCurrentTable = 0L;
long TotalRowsInAllTables = 0L;
long CurrentRowIndexInCurrentTable = 0L;
long CurrentRowIndexInAllTables = 0L;
int TotalTables = 0;
int CurrentTableIndex = 0;
int PercentCompleted = 0;
// ---------------------------
// The Control Centre
// task related message control centre
// ---------------------------
bool task_completed = false;
bool has_error = false;
string err_msg = "";
public Form1()
{
InitializeComponent();
}
// The main button clicked event, initiating the backup process
private void btBackup_Click(object sender, EventArgs e)
{
// 1st thread, this is the main thread that control the UI
// reset the control centre
task_completed = false;
has_error = false;
err_msg = "";
// multi-threading
// 2nd thread, run the backup on another thread for not locking the main UI thread
_ = Task.Run(() => { BeginBackup(); });
}
// running in 2nd thread, starting the main backup process
public void BeginBackup()
{
try
{
string sqlFile = @"D:\backup.sql";
using (var conn = new MySqlConnection(constr))
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
// report the progress 5 times per second
mb.ExportInfo.IntervalForProgressReport = 200;
// subscribe the progress change event
mb.ExportProgressChanged += Mb_ExportProgressChanged;
// run the backup process
mb.ExportToFile(sqlFile);
}
}
catch (Exception ex)
{
// tell the control centre, there is an error
has_error = true;
err_msg = ex.Message;
}
// passing this point indicates the task is completed/stopped
// mark the boolean flag to tell the UI for the completion
task_completed = true;
}
void Mb_ExportProgressChanged(object sender, ExportProgressArgs e)
{
// caching all the values to global variables
CurrentTableName = e.CurrentTableName;
TotalRowsInCurrentTable = e.TotalRowsInCurrentTable;
TotalRowsInAllTables = e.TotalRowsInAllTables;
CurrentRowIndexInCurrentTable = e.CurrentRowIndexInCurrentTable;
CurrentRowIndexInAllTables = e.CurrentRowIndexInAllTables;
TotalTables = e.TotalTables;
CurrentTableIndex = e.CurrentTableIndex;
PercentCompleted = CalculatePercent(TotalRowsInAllTables, CurrentRowIndexInAllTables);
}
int CalculatePercent(long totalValue, long currentValue)
{
if (currentValue >= totalValue)
return 100;
if (totalValue == 0L || currentValue == 0L)
return 0;
return (int)((double)currentValue * 100.0 / (double)totalValue);
}
}
Now, the values have been collected. Next, updating the UI.
Use a timer to update the UI to show the progress status every 200 milliseconds:
// a timer, serves as the 3rd thread for updating the UI
Timer timerUpdateUi = new Timer();
public Form1()
{
InitializeComponent();
timerUpdateUi.Tick += TimerUpdateUi_Tick;
// update the UI 5 times per second
timerUpdateUi.Interval = 200;
}
private void btBackup_Click(object sender, EventArgs e)
{
user_request_cancel_task = false;
task_completed = false;
has_error = false;
err_msg = "";
_ = Task.Run(() => { BeginBackup(); });
// 3rd thread, start the timer in background to intervally update the progress to the UI
timerUpdateUi.Start();
}
// The timer ticked (elapsed) event will update the UI with global cached values
private void TimerUpdateUi_Tick(object sender, EventArgs e)
{
// writing the values to the UI
labelCurrentTableName.Text = CurrentTableName;
labelTotalRowsInCurrentTable.Text = TotalRowsInCurrentTable.ToString();
labelTotalRowsInAllTables.Text = TotalRowsInAllTables.ToString();
labelCurrentRowIndexInCurrentTable.Text = CurrentRowIndexInCurrentTable.ToString();
labelCurrentRowIndexInAllTables.Text = CurrentRowIndexInAllTables.ToString();
labelTotalTables.Text = TotalTables.ToString();
labelCurrentTableIndex.Text = CurrentTableIndex.ToString();
labelPercentCompleted.Text = PercentCompleted.ToString();
// task completion detection
if (task_completed)
{
// task is completed (stopped)
// stop the timer
timerUpdateUi.Stop();
// the task ended with error
if (has_error)
{
MessageBox.Show($"Error: {err_msg}");
}
else
{
MessageBox.Show("Task completed successfully");
}
}
}
Handling task cancellation.
To be able to cancel the task before it's successful completion, simply put another button that trigger the "stop" signal.
private void btStop_Click(object sender, EventArgs e)
{
// set the boolean flag to signal MySqlBackup.NET to stop
user_request_cancel_task = true;
}
At the global variable (the contro centre), add a boolean flag for this:
// ---------------------------
// The Control Centre
// task related message control centre
// ---------------------------
bool user_request_cancel_task = false;
bool task_completed = false;
bool has_error = false;
string err_msg = "";
During the timer ticked event, handle the cancellation situation:
private void TimerUpdateUi_Tick(object sender, EventArgs e)
{
labelCurrentTableName.Text = CurrentTableName;
labelTotalRowsInCurrentTable.Text = TotalRowsInCurrentTable.ToString();
labelTotalRowsInAllTables.Text = TotalRowsInAllTables.ToString();
labelCurrentRowIndexInCurrentTable.Text = CurrentRowIndexInCurrentTable.ToString();
labelCurrentRowIndexInAllTables.Text = CurrentRowIndexInAllTables.ToString();
labelTotalTables.Text = TotalTables.ToString();
labelCurrentTableIndex.Text = CurrentTableIndex.ToString();
labelPercentCompleted.Text = PercentCompleted.ToString();
// task completion detection
if (task_completed)
{
// stop the timer
timerUpdateUi.Stop();
if (has_error)
{
MessageBox.Show($"Error: {err_msg}");
}
else if (user_request_cancel_task)
{
// indicating and handling of task cancellation
MessageBox.Show($"Task is cancelled.");
}
else
{
MessageBox.Show("Task completed successfully");
}
}
}
Here is the extension of C# code for the Restore process that can be patched to above WinForms:
string constr = "server=localhost;user=root;pwd=pwd;database=db;convertzerodatetime=true;treattinyasboolean=true;";
// ---------------------------
// The Control Centre
// task related message control centre
// ---------------------------
bool user_request_cancel_task = false;
bool task_completed = false;
bool has_error = false;
string err_msg = "";
// a timer, serves as the 3rd thread for updating the UI
Timer timerUpdateUi = new Timer();
long CurrentBytes = 0L;
long TotalBytes = 0L;
public Form1()
{
InitializeComponent();
timerUpdateUi.Tick += TimerUpdateUi_Tick;
// update the UI 5 times per second
timerUpdateUi.Interval = 200;
}
private void btRestore_Click(object sender, EventArgs e)
{
// 1st thread, this is the main thread that control the UI
// reset task status control centre
user_request_cancel_task = false;
task_completed = false;
has_error = false;
err_msg = "";
// 2nd thread, run the restore on another thread for not locking the main UI thread
_ = Task.Run(() => { BeginRestore(); });
// 3rd thread, start the timer in background to intervally update the progress to the UI
timerUpdateUi.Start();
}
void BeginRestore()
{
try
{
string sqlFile = @"D:\backup.sql";
using (var conn = new MySqlConnection(constr))
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
// turn off parallel procesing to have a more real time feel
mb.EnableParallelProcessing = false;
// report the progress 5 times per second
mb.ImportInfo.IntervalForProgressReport = 200;
// subscribe the progress change event
mb.ImportProgressChanged += Mb_ImportProgressChanged;
// run the import process
mb.ImportFromFile(sqlFile);
}
}
catch (Exception ex)
{
has_error = true;
err_msg = ex.Message;
}
// passing this point indicates the task is completed/stopped
// mark the boolean flag to tell the UI for the completion
task_completed = true;
}
private void Mb_ImportProgressChanged(object sender, ImportProgressArgs e)
{
CurrentBytes=e.CurrentBytes;
TotalBytes =e.TotalBytes;
// detecting the user cancellation request
if (user_request_cancel_task)
{
// calling the MySqlBackup.NET to stop
((MySqlBackup)sender).StopAllProcess();
}
}
private void TimerUpdateUi_Tick(object sender, EventArgs e)
{
labelTotalBytes.Text = TotalBytes.ToString();
labelCurrentBytes.Text = CurrentBytes.ToString();
// task completion detection
if (task_completed)
{
// stop the timer
timerUpdateUi.Stop();
if (has_error)
{
MessageBox.Show($"Error: {err_msg}");
}
else if (user_request_cancel_task)
{
MessageBox.Show($"Task is cancelled.");
}
else
{
MessageBox.Show("Task completed successfully");
}
}
}
Now, you have understood the fundamentals of the making of progress reporting in Desktop Application.
Next chapters, we'll implement the logic in Web Application.
We have developed a full demo for WinForms, the source code available below: