-
Notifications
You must be signed in to change notification settings - Fork 109
Detailed WinForms Walkthrough ‐ Progress Reporting with MySqlBackup.NET
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
Let's build some more comprehensive UI controls on WinForms
Let's write a slightly more comprehensive intermediary progress status caching layer class. This will store almost everything we'll need. Serves as the bridge between the main MySqlBackup.NET process and the UI.
class TaskResultProgressReport
{
public int TaskType { get; set; }
public DateTime TimeStart { get; set; } = DateTime.MinValue;
public DateTime TimeEnd { get; set; } = DateTime.MinValue;
public TimeSpan TimeUsed
{
get
{
if (TimeStart != DateTime.MinValue && TimeEnd != DateTime.MinValue)
{
return TimeEnd - TimeStart;
}
return TimeSpan.Zero;
}
}
public bool IsRunning { get; set; } = false;
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; } = "";
public int TotalTables { get; set; } = 0;
public int CurrentTableIndex { get; set; } = 0;
public string CurrentTableName { get; set; } = "";
public long TotalRows { get; set; } = 0L;
public long CurrentRows { get; set; } = 0L;
public long TotalRowsCurrentTable { get; set; } = 0L;
public long CurrentRowsCurrentTable { get; set; } = 0L;
public long TotalBytes { get; set; }
public long CurrentBytes { get; set; }
public string TaskName
{
get
{
if (TaskType == 1)
return "Backup";
else if (TaskType == 2)
return "Restore";
return "---";
}
}
public int Percent_TotalRows
{
get
{
if (CurrentRows == 0L || TotalRows == 0L)
return 0;
if (CurrentRows >= TotalRows)
return 100;
if (CurrentRows > 0L && TotalRows > 0L)
return (int)((double)CurrentRows * 100.0 / (double)TotalRows);
return 0;
}
}
public int Percent_TotalRowsTable
{
get
{
if (CurrentRowsCurrentTable == 0L || TotalRowsCurrentTable == 0L)
return 0;
if (CurrentRowsCurrentTable >= TotalRowsCurrentTable)
return 100;
if (CurrentRowsCurrentTable > 0L && TotalRowsCurrentTable > 0L)
return (int)((double)CurrentRowsCurrentTable * 100.0 / (double)TotalRowsCurrentTable);
return 0;
}
}
public int Percent_TotalTables
{
get
{
if (CurrentTableIndex == 0 || TotalTables == 0)
return 0;
if (CurrentTableIndex >= TotalTables)
return 100;
if (CurrentTableIndex > 0 && TotalTables > 0)
return (int)((double)CurrentTableIndex * 100.0 / (double)TotalTables);
return 0;
}
}
public int Percent_TotalBytes
{
get
{
if (CurrentBytes == 0L || TotalBytes == 0L)
return 0;
if (CurrentBytes >= TotalBytes)
return 100;
if (CurrentBytes >= 0L && TotalBytes >= 0L)
return (int)((double)CurrentBytes * 100.0 / (double)TotalBytes);
return 0;
}
}
public string TimeStartDisplay
{
get
{
if (TimeStart != DateTime.MinValue)
{
return TimeStart.ToString("yyyy-MM-dd HH:mm:ss");
}
return "---";
}
}
public string TimeEndDisplay
{
get
{
if (TimeEnd != DateTime.MinValue)
{
return TimeEnd.ToString("yyyy-MM-dd HH:mm:ss");
}
return "---";
}
}
public string TimeUsedDisplay
{
get
{
if (TimeUsed != TimeSpan.Zero)
{
return $"{TimeUsed.Hours}h {TimeUsed.Minutes}m {TimeUsed.Seconds}s {TimeUsed.Milliseconds}ms";
}
return "---";
}
}
public void Reset()
{
TaskType = 0;
TimeStart = DateTime.MinValue;
TimeEnd = DateTime.MinValue;
IsRunning = false;
IsCompleted = false;
IsCancelled = false;
RequestCancel = false;
HasError = false;
ErrorMsg = string.Empty;
TotalTables = 0;
CurrentTableIndex = 0;
TotalRows = 0;
CurrentRows = 0;
TotalRowsCurrentTable = 0;
CurrentRowsCurrentTable = 0;
TotalBytes = 0;
CurrentBytes = 0;
}
}
We'll declare the class of TaskResultProgressReport
at the global class level with volatile
modifier.
In C#, the volatile
keyword is a modifier that makes the field thread safe, which it can safely be accessed (read and write) by multiple threads.
In our case, it is indeed going to be read and write by two main threads, one thread represents MySqlBackup.NET which will write status data to it, and another thread represents the main UI which it will read the values to update the UI components.
public partial class FormProgressReport : baseForm
{
// global class level data caching
volatile TaskResultProgressReport taskInfo = new TaskResultProgressReport();
// a global timer serves as another thread for updating the progress values
Timer timerUpdateProgress = new Timer();
public FormProgressReport()
{
InitializeComponent();
// 200 milliseconds
// update 5 times per second
timerUpdateProgress.Interval = 200;
// run the update for each time elapsed
timerUpdateProgress.Tick += TimerUpdateProgress_Tick;
}
}
The timer (3rd thread) triggered for updating the UI:
private void TimerUpdateProgress_Tick(object sender, EventArgs e)
{
// obtaining values from global caching data and update the UI components
lbTaskType.Text = taskInfo.TaskName;
lbTimeStart.Text = taskInfo.TimeStartDisplay;
lbTimeEnd.Text = taskInfo.TimeEndDisplay;
lbTimeUsed.Text = taskInfo.TimeUsedDisplay;
lbIsCompleted.Text = taskInfo.IsCompleted.ToString();
lbIsCancelled.Text = taskInfo.IsCancelled.ToString();
lbHasError.Text = taskInfo.HasError.ToString();
lbErrorMsg.Text = taskInfo.ErrorMsg;
if (taskInfo.TaskType == 1)
{
lbTotalRows.Text = $"{taskInfo.CurrentRows} / {taskInfo.TotalRows} ({taskInfo.Percent_TotalRows}%)";
lbTotalTables.Text = $"{taskInfo.CurrentTableIndex} / {taskInfo.TotalTables} ({taskInfo.Percent_TotalTables}%)";
lbRowsCurrentTable.Text = $"{taskInfo.CurrentRowsCurrentTable} / {taskInfo.TotalRowsCurrentTable} ({taskInfo.Percent_TotalRowsTable}%)";
progressBar_TotalRows.Value = taskInfo.Percent_TotalRows;
progressBar_TotalTables.Value = taskInfo.Percent_TotalTables;
progressBar_RowsCurrentTable.Value = taskInfo.Percent_TotalRowsTable;
}
else
{
lbTotalBytes.Text = $"{taskInfo.CurrentBytes} / {taskInfo.TotalBytes} ({taskInfo.Percent_TotalBytes}%)";
progressBar_TotalBytes.Value = taskInfo.Percent_TotalBytes;
}
// task completion detection
// this is where UI thread will call the timer to stop updating values
if (taskInfo.IsCompleted)
{
// stop the timer
timerUpdateProgress.Stop();
// displaying the message somewhere in the UI
// require your own implementation
WriteStatus($"{taskInfo.TaskName} task is stopped/completed");
}
if (taskInfo.HasError)
{
// displaying additional message somewhere in the UI
WriteStatus($"{taskInfo.TaskName} task has error");
WriteStatus($"{taskInfo.ErrorMsg}");
}
if (taskInfo.IsCancelled)
{
WriteStatus($"{taskInfo.TaskName} is cancelled by user");
}
}
The UI is being updated...
Next, the two main buttons that will begin the process in another background thread:
private void btBackup_Click(object sender, EventArgs e)
{
string sqlFilePath = @"D:\backup.sql";
// reset all the UI into initial state
ResetUI();
int taskType = 1; // backup
// run the task in background thread
_ = Task.Run(() => { BeginTask(taskType, txtConstr.Text, sqlFilePath); });
// start the timer to update the UI
timerUpdateProgress.Start();
}
private void btRestore_Click(object sender, EventArgs e)
{
string sqlFilePath = @"D:\backup.sql";
// reset all the UI into initial state
ResetUI();
int taskType = 2; // restore
// run the task in another thread (run in background)
_ = Task.Run(() => { BeginTask(taskType, txtConstr.Text, sqlFilePath); });
// start the timer
timerUpdateProgress.Start();
}
Running the backup and restore task in background:
void BeginTask(int taskType, string constr, string sqlFile)
{
// first, mark the task is busy running
taskInfo.IsRunning = true;
// reset all values
taskInfo.Reset();
taskInfo.TaskType = taskType;
taskInfo.TimeStart = DateTime.Now;
try
{
using (var conn = new MySqlConnection(constr))
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
if (taskType == 1)
{
mb.ExportInfo.IntervalForProgressReport = 200;
mb.ExportProgressChanged += Mb_ExportProgressChanged;
mb.ExportToFile(sqlFile);
}
else if (taskType == 2)
{
// for better real-time accuracy
mb.ImportInfo.EnableParallelProcessing = false;
mb.ImportInfo.IntervalForProgressReport = 200;
mb.ImportProgressChanged += Mb_ImportProgressChanged;
mb.ImportFromFile(sqlFile);
}
}
}
catch (Exception ex)
{
taskInfo.HasError = true;
taskInfo.ErrorMsg = ex.Message;
}
// reaching this line, MySqlBackup.NET has already finished it's process
// it could be either a cancelled,
// or a complete success task
// identifying the cancellation request
if (taskInfo.RequestCancel)
{
// officially mark the task as cancelled
taskInfo.IsCancelled = true;
}
taskInfo.TimeEnd = DateTime.Now;
// officially announcing the completion,
// so that the UI thread can use this reference to stop the timer
taskInfo.IsCompleted = true;
// task completed
taskInfo.IsRunning = false;
}
// writing all the progress status values to the global caching class
private void Mb_ExportProgressChanged(object sender, ExportProgressArgs e)
{
taskInfo.TotalRows = e.TotalRowsInAllTables;
taskInfo.CurrentRows = e.CurrentRowIndexInAllTables;
taskInfo.TotalTables = e.TotalTables;
taskInfo.CurrentTableIndex = e.CurrentTableIndex;
taskInfo.TotalRowsCurrentTable = e.TotalRowsInCurrentTable;
taskInfo.CurrentRowsCurrentTable = e.CurrentRowIndexInCurrentTable;
taskInfo.CurrentTableName = e.CurrentTableName;
// detecting the cancellation signal
if (taskInfo.RequestCancel)
{
// signal MySqlBackup.NET to terminate
((MySqlBackup)sender).StopAllProcess();
}
}
private void Mb_ImportProgressChanged(object sender, ImportProgressArgs e)
{
taskInfo.TotalBytes = e.TotalBytes;
taskInfo.CurrentBytes = e.CurrentBytes;
// detecting the cancellation signal
if (taskInfo.RequestCancel)
{
// signal MySqlBackup.NET to terminate
((MySqlBackup)sender).StopAllProcess();
}
}
The termination request:
private void btStop_Click(object sender, EventArgs e)
{
if (taskInfo.IsRunning)
{
// mark the boolean flag to signal the termination/cancellation
taskInfo.RequestCancel = true;
}
}
Done.