I have a WPF
application, and I'm using a BackgroundWorker
component to retrieve some data from the back-end and display it in the UI.
The BackgroundWorker
has WorkerReportsProgress = true
so that the UI can be updated periodically. The BackgroundWorker
also has WorkerSupportsCancellation = true
so that it can be Cancelled by the user. All that works great!
I am having trouble trying to implement a third and more complex behavior. Basically, the user needs to have the flexibility to start a new BackgroundWorker
task at any time including while one is currently executing. If a task is currently executing and a new one is started, the old task needs to be marked as Aborted
. Aborted
tasks are different from Cancelled
in that Aborted
is not allowed to make any further UI updates. It should be "cancelled silently".
I wrapped the BackgroundWorker
inside the AsyncTask
class and added the IsAborted
bit. Checking against the IsAborted
bit inside ProgressChanged
and RunWorkerCompleted
prevents further UI updates. Great!
However, this approach breaks down because when new tasks are started up, CurrentTask
is replaced with a new instance of AsyncTask
. As a result, it becomes difficult to track the CurrentTask
.
As noted, in the TODO:
, it's almost like I want to wait until the CurrentTask
completes after an abort before starting a new task. But I know this will provide a bad user experience as the UI thread will be blocked until the old task completes.
Is there a better way to track multiple AsyncTasks
ensuring that new ones can be fired up on demand and old ones are Aborted correctly with no further UI updates? There doesn't seem to be a good way to track the CurrentTask... Does that TPL offer a better way to handle what I'm after?
Here are the notable snippets I have inside my Window class:
private AsyncTask CurrentTask { get; set; }
private class AsyncTask
{
private static int Ids { get; set; }
public AsyncTask()
{
Ids = Ids + 1;
this.Id = Ids;
this.BackgroundWorker = new BackgroundWorker();
this.BackgroundWorker.WorkerReportsProgress = true;
this.BackgroundWorker.WorkerSupportsCancellation = true;
}
public int Id { get; private set; }
public BackgroundWorker BackgroundWorker { get; private set; }
public bool IsAborted { get; set; }
}
void StartNewTask()
{
if (this.CurrentTask != null && this.CurrentTask.BackgroundWorker.IsBusy)
{
AbortTask();
//TODO: should we wait for CurrentTask to finish up? this will block the UI?
}
var asyncTask = new AsyncTask();
asyncTask.BackgroundWorker.DoWork += backgroundWorker_DoWork;
asyncTask.BackgroundWorker.ProgressChanged += backgroundWorker_ProgressChanged;
asyncTask.BackgroundWorker.RunWorkerCompleted += backgroundWorker_RunWorkerCompleted;
AppendText("Starting New Task: " + asyncTask.Id);
this.CurrentTask = asyncTask;
asyncTask.BackgroundWorker.RunWorkerAsync();
}
void AbortTask()
{
if (this.CurrentTask != null && this.CurrentTask.BackgroundWorker.IsBusy)
{
AppendText("Aborting Task " + this.CurrentTask.Id + "...");
this.CurrentTask.IsAborted = true;
this.CurrentTask.BackgroundWorker.CancelAsync();
}
}
void CancelTask()
{
if (this.CurrentTask != null && this.CurrentTask.BackgroundWorker.IsBusy)
{
AppendText("Cancelling Task " + this.CurrentTask.Id + "...");
this.CurrentTask.BackgroundWorker.CancelAsync();
}
}
void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
var backgroundWorker = (BackgroundWorker)sender;
for (var i = 0; i < 10; i++)
{
//check before making call...
if (backgroundWorker.CancellationPending)
{
e.Cancel = true;
return;
}
//simulate a call to remote service...
Thread.Sleep(TimeSpan.FromSeconds(10.0));
//check before reporting any progress...
if (backgroundWorker.CancellationPending)
{
e.Cancel = true;
return;
}
backgroundWorker.ReportProgress(0);
}
}
void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
if (this.CurrentTask.IsAborted)
return;
AppendText("[" + DateTime.Now.ToString("MM/dd/yyyy HH:mm:ss") + "] " + "Progress on Task: " + this.CurrentTask.Id + "...");
}
void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (this.CurrentTask.IsAborted)
return;
if (e.Cancelled)
{
AppendText("Cancelled Task: " + this.CurrentTask.Id);
}
else if (e.Error != null)
{
AppendText("Error Task: " + this.CurrentTask.Id);
}
else
{
AppendText("Completed Task: " + this.CurrentTask.Id);
}
//cleanup...
this.CurrentTask.BackgroundWorker.DoWork -= backgroundWorker_DoWork;
this.CurrentTask.BackgroundWorker.ProgressChanged -= backgroundWorker_ProgressChanged;
this.CurrentTask.BackgroundWorker.RunWorkerCompleted -= backgroundWorker_RunWorkerCompleted;
this.CurrentTask= null;
}
From what I understand, you don't want to actually abort the thread, you just want it to continue working silently (i.e. not update the UI)? One way would be to keep a list of BackgroundWorkers and remove their event handlers if they're to be "aborted".
List<BackgroundWorker> allBGWorkers = new List<BackgroundWorker>();
//user creates a new bg worker.
BackgroundWorker newBGWorker = new BackgroundWorker();
//.... fill out properties
//before adding the new bg worker to the list, iterate through the list
//and ensure that the event handlers are removed from the existing ones
foreach(var bg in allBGWorkers)
{
bg.ProgressChanged -= backgroundWorker_ProgressChanged;
bg.RunWorkerCompleted -= backgroundWorker_RunWorkerCompleted;
}
//add the latest bg worker you created
allBGWorkers.Add(newBGWorker);
That way you can keep track of all the workers. Since a List
maintains order, you'll know which one the latest on is (the last one in the list) but you can just as easily use a Stack
here if you prefer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With