title: Thread synchronization between UI and a worker thread
body: <p>I have a service that performs some complex automation against 3rd party software (via .NET UIAutomation). The service consists of a host which spins up another STA Thread to run the automation on while the host waits via a <code>ManualResetEvent</code>. This was working great until the decision was made to use this in a user-facing environment. At that point, I created an <code>AutomationMessenger</code> class to drive status updates out to the UI/WPF app. This worked well. Then at some point, I needed to get and validate user input mid-automation and drive it back down to the automation thread- this is where the wheels have fallen off the bus. I added a call back action to the <code>AutomationMessenger</code>, and all seemed well. I ended up shuffling things around a bit, and the user input was needed at a different point in the automation. Ultimately, the methods in the automation classes began to get called twice- So I knew I had some threading issues at play.</p>
  <p>Not surprisingly, these issues are not present in testing, or when the service is run without raising user notifications.</p>
  <p>The Threads are created as expected and sending messages from the automation up to the UI layer to be invoked by <code>Application.Current.Dispatcher</code> works to update the UI, but when a callback action within the automation code/thread is invoked on the UI thread, control is never returned to that thread. I can see it happening in the Threads window in VS.</p>
  <p>This is not surprising to me, I just can't figure out how to fix it.</p>
  <p>It's taken me some effort to boil the complexity of this code down to a Minimal, Reproducible Example, but I have managed, and it illustrates the issue.</p>
  <p><a href="https://github.com/AndyStagg/AutomationThreadingIssue" rel="nofollow noreferrer">GitHub - AutomationThreadingIssue</a></p>
  <p>I've spent considerable time reading about <code>SynchronizationContext</code>s, and I thought that it was going to be as simple as passing the context from the automation thread up to the UI to <code>Send</code>/<code>Post</code> to it, but learned that worker threads don't automatically get a context.</p>
  <p>Simply calling Join() on the automation thread seems to kill the automation thread, or merge it into the UI thread.</p>
  <p>Starting a new Task (<code>Task.Run(() =&gt; Callback)</code>) from the UI thread is the best solution I have at the moment to keep the UI/automation threads separate, but that's creating multiple hits back into some of the subsequent automation methods which respond to windows/dialogs popping up in the 3rd party software (via <code>Automation.AddAutomationEventHandler</code>).</p>
  <p>After searching for all possibilities of &quot;cross-thread messaging&quot;, the consensus is a <code>BlockingCollection</code>/<code>ConcurrentQueue</code>, or <code>Channels</code>. The project is limited to net45, which rules out the channels (minimum requirement is 4.6). And I couldn't get a blocking collection implementation working without blocking execution in the automation thread.</p>
  <p>I see a lot of solutions for invoking a delegate/action back on a UI thread from a worker thread, but nothing useful for invoking a method from the UI thread back on a worker thread.</p>
  <pre><code>public partial class MainWindow : Window
  {
      public MainWindow()
      {
          InitializeComponent();
  
          Thread.CurrentThread.Name = &quot;WPF Thread&quot;;
  
          AutomationMessenger.Received += AutomationMessenger_Received;
      }
  
      private void AutomationMessenger_Received(object sender, AutomationMessengerEventArgs e)
      {
          switch (e.Name)
          {
              case &quot;Status&quot;:
                  Application.Current.Dispatcher.Invoke(() =&gt;
                  {
                      Status_TextBlock.Text = e.Message;
                  });
  
                  break;
  
              case &quot;UserInput&quot;:
                  Application.Current.Dispatcher.Invoke(() =&gt;
                  {
                      AutomationInput_Grid.IsEnabled = true;
  
                      Input_Button.Tag = e.CallbackAction;
  
                      Status_TextBlock.Text = e.Message;
                  });
  
                  break;
          }
      }
  
      private void Run_Button_Click(object sender, RoutedEventArgs e)
      {
          Run_Button.IsEnabled = false;
  
          var backgroundAutomationTask = new Task(() =&gt;
          {
              var worker = new AutomationBackgroundWorker();
              worker.Begin();
          });
  
          backgroundAutomationTask.Start();
      }
  
      private void Input_Button_Click(object sender, RoutedEventArgs e)
      {
          if (Input_Button.Tag != null &amp;&amp; Input_Button.Tag is Action&lt;string&gt; callbackAction)
          {
              callbackAction.Invoke(Input_TextBox.Text);
          }
      }
  }
  </code></pre>
  <pre><code>public class AutomationBackgroundWorker
  {
      private ManualResetEvent manualResetEvent;
      private Thread automationThread;
  
      private SynchronizationContext backgroundWorkerContext;
  
      public AutomationBackgroundWorker()
      {
          Thread.CurrentThread.Name = &quot;Automation Background Worker&quot;;
          backgroundWorkerContext = SynchronizationContext.Current;
  
          automationThread = new Thread(this.Automation);
          automationThread.Name = &quot;Automation Thread&quot;;
          automationThread.SetApartmentState(ApartmentState.STA);
  
          manualResetEvent = new ManualResetEvent(false);
      }
  
      public void Begin()
      {
          automationThread.Start();
  
          manualResetEvent.WaitOne();
      }
  
      public void Complete()
      {
          manualResetEvent.Set();
          manualResetEvent.Close();
      }
  
      public void Automation()
      {
          var automationSyncContext = SynchronizationContext.Current;
  
          AutomationMessenger.Send(&quot;Status&quot;, &quot;Initializing&quot;, null);
  
          // Initialize Automation Elements, etc..
          Task.Delay(1000).Wait();
  
          AutomationMessenger.Send(&quot;Status&quot;, &quot;Initialized&quot;, null);
  
          // Get user input
          AutomationMessenger.Send(&quot;UserInput&quot;, &quot;Please Enter Your Name&quot;, UserInputCallback);
  
          // Wait for their response
      }
  
      // As you would expect, this method is being called from the WPF Thread / UI Thread
      // How can I synchronize it back to the Automation Thread
      private void UserInputCallback(string userInput)
      {
          //if (Thread.CurrentThread != automationThread)
          //{
          //    automationThread.Join();
          //}
          // Doing this blocks and kills the Automation Thread
  
          if (string.IsNullOrWhiteSpace(userInput)) return;
  
          AutomationMessenger.Send(&quot;Status&quot;, $&quot;Thanks {userInput}&quot;, null);
  
          Complete();
      }
  }
  </code></pre>
  <pre><code>public static class AutomationMessenger
  {
      public static event AutomationMessengerEventHandler Received;
      public static void Send(string name, string message, Action&lt;string&gt; callbackAction) =&gt; Received?.Invoke(null, new AutomationMessengerEventArgs(name, message, callbackAction));
  }
  
  public delegate void AutomationMessengerEventHandler(object sender, AutomationMessengerEventArgs e);
  
  public class AutomationMessengerEventArgs : EventArgs
  {
      public string Name { get; set; }
      public string Message { get; set; }
      public Action&lt;string&gt; CallbackAction { get; set; }
      public AutomationMessengerEventArgs(string name, string message, Action&lt;string&gt; callbackAction) : base()
      {
          Name = name;
          Message = message;
          CallbackAction = callbackAction;
      }
  }
  </code></pre>