Search code examples
c#winformsmultithreadingtask-parallel-librarytask

WinForms TPL Pattern w/ Multiple Tasks & UI Sync - is this correct?


I'm new to the TPL (Task-Parallel Library) and am wondering if the following is the most efficient way to spin up 1 or more tasks, collate the results, and display them in a datagrid.

  1. Search1 & Search2 talk to two separate databases, but return the same results.
  2. I disable the buttons and turn on a spinner.
  3. I'm firing the tasks off using a single ContinueWhenAll method call.
  4. I've added the scheduler to the ContinueWhenAll call to update form buttons, datagrid, and turn off the spinner.

Q: Am I doing this the right way ? Is there a better way ?
Q: How could I add cancellation/exception checking to this ?
Q: If I needed to add progress reporting - how would I do that ?

The reason that I chose this method over say, a background worker is so that I could fire each DB task off in parallel vs. sequentially. Besides that, I thought it might be fun to use the TPL.. however, since I could not find any concrete examples of what I'm doing below (multiple tasks) I thought it might be nice to put it on here to get the answers, and hopefully be an example for others.

Thank you!

Code:

//  Disable buttons and start the spinner
btnSearch.Enabled = btnClear.Enabled = false;
searchSpinner.Active = searchSpinner.Visible = true;

//  Setup scheduler
TaskScheduler scheduler = TaskScheduler.FromCurrentSynchronizationContext();

//  Start the tasks
Task.Factory.ContinueWhenAll(
  //  Define the search tasks that return List<ImageDocument>
  new [] {  
    Task.Factory.StartNew<List<ImageDocument>>(Search1), 
    Task.Factory.StartNew<List<ImageDocument>>(Search2) 
  }, 
  //  Process the return results
  (taskResults) => {
    //  Create a holding list
    List<ImageDocument> documents = new List<ImageDocument>();
    //  Iterate through the results and add them to the holding list
    foreach (var item in taskResults) {
      documents.AddRange(item.Result);
    }
    //  Assign the document list to the grid
    grid.DataSource = documents;
    //  Re-enable the search buttons
    btnSearch.Enabled = btnClear.Enabled = true;
    //  End the spinner
    searchSpinner.Active = searchSpinner.Visible = false;
  }, 
  CancellationToken.None, 
  TaskContinuationOptions.None, 
  scheduler
);

Solution

  • Q: Am I doing this the right way ? Is there a better way ?

    Yes, this is a good way to handle this type of situation. Personally, I would consider refactoring the disable/enable of the UI into a separate method, but other than that, this seems very reasonable.

    Q: How could I add cancellation/exception checking to this ?

    You could pass around a CancellationToken to your methods, and have them check it and throw if a cancellation was requested.

    You'd handle exceptions where you grab the results from taskResults. This line:

      documents.AddRange(item.Result);
    

    Is where the exception will get thrown (as an AggregateException or OperationCanceledException) if an exception or cancellation occurred during the operations.

    Q: If I needed to add progress reporting - how would I do that ?

    The simplest way would be to pass the scheduler into your methods. Once you've done that, you could use it to schedule a task that updates on the UI thread - ie: Task.Factory.StartNew with the TaskScheduler specified.


    however, since I could not find any concrete examples of what I'm doing below (multiple tasks)

    Just FYI - I have samples of working with multiple tasks in Part 18 of my series on TPL.