6

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;
}
4

1 回答 1

11

据我了解,您并不想真正中止线程,您只是希望它继续静默工作(即不更新 UI)?一种方法是保留 BackgroundWorkers 的列表,如果要“中止”它们,则删除它们的事件处理程序。

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);

这样你就可以跟踪所有的工人。由于 aList维护顺序,您会知道最新的是哪一个(列表中的最后一个),但Stack如果您愿意,也可以在此处轻松使用 a。

于 2013-06-04T13:49:53.133 回答