Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the BackgroundWorker not call the RunWorkerCompleted on the right thread in this unit test?

The whole point of the backgroundWorker is to update the UI after a time-consuming task. The component works as advertised in my WPF app.

However in my test, the callback is not invoked on the calling thread.

[Test]
public void TestCallbackIsInvokedOnClientThread()
{

     var clientId = Thread.CurrentThread.ManagedThreadId;
     int callbackThreadId = -1;
     var manualEvent = new ManualResetEventSlim(false);

     var someUIControl = new TextBox();
     var bw = new BackgroundWorker();

     bw.DoWork += (s,e) => e.Result = 5 ; // worker thread

     bw.RunWorkerCompleted += (s, e) =>
                                  {
                                      try
                                      {
                                          callbackThreadId = Thread.CurrentThread.ManagedThreadId;
                                          //someUIControl.Text = callbackThreadId.ToString();
                                          manualEvent.Set();
                                      }
                                      catch (System.Exception ex)
                                      {
                                          Console.Out.WriteLine(ex.ToString());
                                      }
                                  };
     bw.RunWorkerAsync();

     if (!manualEvent.Wait(5000))
         Assert.Fail("no callback");
     Assert.AreEqual(clientId, callbackThreadId);
 }

Result Message: Assert.AreEqual failed. Expected:<15>. Actual:<10>. callback not invoked on client Thread

What am I missing ?

In the Unit Test I see behavior like

------ Run test started ------
MainThread Id =21
Worker Thread Id =9
Callback Thread Id =9

In the Wpf App, this would be

MainThread Id =1
Worker Thread Id =14
Callback Thread Id =1

Update: With Justin's answer, made the following changes and now the test passes

  • Before creating the BackgroundWorker SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(control.Dispatcher));
  • Instead of using a event for signalling between the threads, simulate a message pump

.

for (int i = 0; i < 3; i++)
{
    control.Dispatcher.Invoke(DispatcherPriority.Background,
                                          new Action(delegate { }));
    Thread.Sleep(50);
}
like image 590
Gishu Avatar asked Nov 23 '12 10:11

Gishu


2 Answers

The behavior is different dues to the different contexts that you are running under.

When you call bw.RunWorkerAsync(), the SynchronizationContext is captured. This is used to dispatch out the RunWorkerCompleted call.

Under WPF it will use DispatcherSynchronizationContext which will marshall the completed call back to the UI thread. Under the test, this marshalling is unnecessary so it remains on the background worker thread.

like image 54
Justin Harvey Avatar answered Sep 21 '22 03:09

Justin Harvey


I belive that the calling thread must support messagepumping (mean, being STA apartment and having an associated Dispatcher) so the background worker can post the callback. If it does not, the background worker has no option but execute the callback in its own thread. If you want to test it, see this link.

like image 40
Arthur Nunes Avatar answered Sep 22 '22 03:09

Arthur Nunes