Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What Is the appropriate way to report progress to a WPF view from a TPL task?

I am trying to read a large number of rows from a database in small "chunks" or pages and report the progress to the user; i.e., if I'm loading 100 "chunks" report progress as each chunk is loaded.

I am using the TPL in C# 4.0 to read these chunks from the database and then hand off the full result set to another task that can use it. I feel that the TPL gives me better control over task cancellation and hand off than a BackgroundWorker, etc., but there doesn't seem to be a built in way to report the progress of a task.

Here is the solution I've implemented to report progress to a WPF progress bar and I want to make sure this is appropriate and that there's not a better approach I should be taking.

I started by creating a simple interface to represent changing progress:

public interface INotifyProgressChanged
{
  int Maximum { get; set; }
  int Progress { get; set; }
  bool IsIndeterminate { get; set; }
}

These properties can be bound to a ProgressBar in a WPF view and the interface is implemented by the backing ViewModel which is responsible for initiating the loading of the data and eventually reporting the overall progress (simplified for this example):

public class ContactsViewModel : INotifyProgressChanged
{
  private IContactRepository repository;

  ...

  private void LoadContacts()
  {
    Task.Factory.StartNew(() => this.contactRepository.LoadWithProgress(this))
      .ContinueWith(o => this.UseResult(o));
  }
}

You'll notice that I'm passing the ViewModel to the repository method as an INotifyProgressChanged and this is where I want to make sure I'm not doing something wrong.

My thought process here is that in order to report progress, the method actually doing the work (which is the repository method) needs access to the INotifyProgressChanged interface in order to report progress ultimately updating the view. Here's a quick look at the repository method (shortened for this example):

public class ContactRepository : IContactRepository
{
  ...

  public IEnumberable<Contact> LoadWithProgress(INotifyProgressChanged indicator)
  {
    var resultSet = new List<Contact>();
    var query = ... // This is a LINQ to Entities query

    // Set the maximum to the number of "pages" that will be iterated
    indicator.Maximum = this.GetNumberOfPages(query.Count(), this.pageSize);

    for (int i = 0; i < indicator.Maximum; i++)
    {
      resultSet.AddRange(query.Skip(i * this.pageSize).Take(this.pageSize));
      indicator.Progress += 1; // As each "chunk" is loaded, progress is updated
    }

    // The complete list is returned after all "chunks" are loaded
    return resultSet.AsReadOnly();
  }
}

And that is how the repository is ultimately reporting progress to the View through the ViewModel. Is this the right approach? Am I using the TPL properly, violating any major rules, etc.? This solution is working, progress is being reported as expected, I just want to make sure I'm not setting myself up for failure.

like image 339
sellmeadog Avatar asked Jan 30 '26 04:01

sellmeadog


2 Answers

The "prescribed" way of doing this is passing the TaskScheduler instance from TaskSheduler::FromCurrentSynchronizationContext to ContinueWith that you want to ensure executes on the WPF dispatcher thread.

For example:

 public void DoSomeLongRunningOperation()
 {
    // this is called from the WPF dispatcher thread

    Task.Factory.StartNew(() =>
    {
        // this will execute on a thread pool thread
    })
    .ContinueWith(t =>
    {
        // this will execute back on the WPF dispatcher thread
    },
    TaskScheduler.FromCurrentSynchronizationContext());
 }
like image 151
Drew Marsh Avatar answered Jan 31 '26 17:01

Drew Marsh


I recommend that you avoid updating data-bound properties from background threads.

To solve this, you can have your background task post a UI task to do its update, or (even better), use the IProgress<T>/Progress<T> system described in the Task-Based Asynchronous Pattern Overview document.

The IProgress<T> approach is nice because it separates your background task from the ViewModel updates. However, it has some drawbacks (sharing data between the background task and the update; and handling exceptions from the update); I'm hoping these are addressed before the official Async CTP release.

like image 41
Stephen Cleary Avatar answered Jan 31 '26 17:01

Stephen Cleary



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!