Matthias
Matthias

Reputation: 280

BackgroundWorker RunWorkerCompleted not executed on UI thread

I have written a small wrapper around BackgroundWorker:

public class Worker
{
  public void Execute<T>(Func<T> work, Action<Exception, T> workCompleted)
  {
    var worker = new BackgroundWorker();
    worker.DoWork += (o, ea) => { ea.Result = work(); };
    worker.RunWorkerCompleted += (o, ea) => { workCompleted(ea.Error, (T)ea.Result); };
    worker.RunWorkerAsync();
  }
}

I use it in the viewmodel (which is created in XAML) like so:

public class MainWindowViewmodel : INotifyPropertyChanged
{
  private ObservableCollection<Job> _jobs = new ObservableCollection<Job>();
  public ICollectionView JobsView { get; } = CollectionViewSource.GetDefaultView(_jobs);
  public RelayCommand RefreshJobsCommand => new RelayCommand(x => UpdateJobs()); /*edit1*/

  private void UpdateJobs() /*executed via command binding*/
  {
    new Worker().Execute(() => _webServiceManager.JobService.GetJobsByDate(settings.Limit, DateTime.Now.AddDays(-365)), (ex, jobArray) =>
      {
        jobArray.toList().ForEach(j=>_jobs.Add(j));
      });
  }
}

When jobs are added in workCompleted to ObservableCollection _jobs, I get the following exception: This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.

So obviously the RunWorkerCompleted callback doesn't run on the UI thread. But as far as I know it should run on the one thread from which RunWorkerAsync was called? And this should be the UI thread, since it is called from inside the viewmodel (which was created from XAML) from a command binding (also UI thread), right?

What am I missing here?

Edit2: I could narrow down the problem to the initial loading of jobs. This is done from the ctor of MainWindow and will result in the described problem. Any additional updates afterwards using the UI button via command binding work absolutely fine.

So the problem is, on initial loading, MainWindow ctor, ViewModel ctor and Worker.Execute() are executed on the UI thread, but workCompleted is not, although the SynchronizationContext should be set to the UI thread at the moment of invoking RunWorkerAsync.

Whenever the next loading is triggered via command binding, the workCompleted is also set to the SynchronizationContext of the UI thread.

Any idea why this isn't working in the first place?

public partial class MainWindow : Window
{
   private MainWindowViewmodel _viewmodel;

   public MainWindow(Connection connection)
   {
      InitializeComponent();
      Debug.WriteLine("mainwindow-ctor: " + Thread.CurrentThread.ManagedThreadId.ToString());
      _viewmodel = (MainWindowViewmodel)DataContext;
      _viewmodel.UpdateJobs();
   }
}

Upvotes: 0

Views: 577

Answers (2)

Matthias
Matthias

Reputation: 280

Solved the problem by moving the initial loading of the jobs from the MainWindow constructor to the MainWindow.Loaded event.

For some reason the BackgroundWorker doesn't have the SynchronizationContext on the UI thread for RunWorkerCompleted unless the UI is fully initialized.

public partial class MainWindow : Window
{
   private MainWindowViewmodel _viewmodel;

   public MainWindow(Connection connection)
   {
      InitializeComponent();
      _viewmodel = (MainWindowViewmodel)DataContext;
      Loaded += MainWindowLoaded;
   }

   private void MainWindowLoaded(object sender, RoutedEventArgs e)
   {
      _viewmodel.UpdateJobs();
   }
}

Upvotes: 1

mm8
mm8

Reputation: 169200

The thread on which the RunWokerCompleted event is handled is determined by SynchronizationContext.Current property at the time the RunWorkerAsync method is called.

So if your Execute method is actually called on the UI thread in a WPF application, where the SynchronizationContext.Current property returns a DispatcherSynchronizationContext, workCompleted will be invoked on this thread as well. You easily can confirm this yourself by calling Execute() in the constructor of the MainWindow:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        new Worker().Execute(() => { Thread.Sleep(5000); return 0; }, (ex, jobArray) =>
        {
            MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString());
        });
    }
}

By the way, the Task Parallel Library (TPL) is the preferred way to write multithreaded and parallel code since .NET Framework 4. The task asynchronous programming model (TAP) and the async and await keywords that were introdued in .NET 4.5 make it even easier to use.

Upvotes: 3

Related Questions