Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tracking Multiple BackgroundWorkers

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;
}
like image 391
John Russell Avatar asked Jun 04 '13 13:06

John Russell


1 Answers

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.

like image 73
keyboardP Avatar answered Nov 17 '22 22:11

keyboardP