Search code examples
c#wpfoffice-interop

UI thread vs background thread - UI control accessibility boundary


It may well be the case that my question can't be answered in general, and if so, that's interesting information regardless. I'm trying to understand UI control interaction better, specifically what is and what isn't allowed to be performed by non UI threads. In my case I'm interacting with both WPF controls and with objects (that represent controls) that lie in the Microsoft.Office.Interop namespace. I'm doing a lot of work on background threads in my model layer, but when events are received in my view-models, I'm dispatching on the UI thread (this it typically when working with properties that are bound to WPF controls). That makes sense to me and it seems to work well. I'm uncertain however, about what can and cannot be done by background threads. Essentially, I'm trying to understand where the boundary lies. To get the imagination going, see some questions below. I understand that my request is a bit open-ended, so if someone knows an external resource I can read that's just as good to me.

  • Can I get() on a property bound to a ui from a background thread, i.e. is it only set() that must be done from the UI thread?
  • If I have an interop object, say, Microsoft.Office.Interop.Slide, it seems to me that I have to interact with it on the UI thread because it's taken be an actual ui control. But can I listen to events that return such objects in non-ui threads? Can I check a Slide for null from a non-ui thread? Can I get() on properties contained within a Slide type from a non-ui thread?

In summary, is there a good rule I can keep in mind when switching between background threads and the UI thread? For instance "any action that changes a ui control must be done on the ui thread", or "any action that directly interacts, regardless of action (e.g. check for null) with a ui control must be done on the ui thread".


Solution

  • Anything that interacts with the UI is not thread-safe and trying to cut corners will get you in trouble. You may think that a property getter is innocent but it is not. Nothing pretty happens when the user continues to interact with the UI while the thread is running, you'll compute a result from a stale value and present a result that is inconsistent with the UI state. You'll know to not change a value that's critical to what the thread does, your user won't.

    No lock can fix that, you can't put a lock in the UI thread and you can't put a lock on the user. Except the obvious one, you need to write the code that prevents the user from changing the value while the thread trundles. Which now makes it quite unnecessary to obtain the value inside the thread, you might as well obtain it before you start the thread. A lambda expression is quite handy to pass it to the threaded code.

    You certainly can allow the user to change the UI but then you have to write much more complicated threading code. You have to ensure that the current thread stops and a new one starts, now using the updated value. Pretty hard to do correctly, thread stops are not instantaneous and liable to deadlock.

    Same applies for the thread's result, there is generally no point in updating the UI inside the thread. The BackgroundWorker and Task classes help you start code on the UI thread after the thread is done, it can safely update the UI with the result. Letting threads do small things that are easy to think through and easy to interlock is very important to stay out of trouble.

    Office interop is a bit different from WPF, thread-safety is automatic in COM. It automagically calls the equivalent of Dispatcher.Invoke() and no additional help is required. And an Office document is much more likely to remain static while the worker thread runs. Except when you allow the user to also change the document, then you have the getter problems back in spades. YMMV.