'VSTO with Windows Form and Worker Threads
I have an Office addin (Outlook if it makes a difference) created with VSTO. The addin creates a Windows Form at startup. My Form creates several threads. I'm looking for either concrete guidance or peoples' experiences with what safely works for the following situations:
1.) A thread created by the Form needs to access the Office object model (Globals.ThisAddIn.Application)
2.) A thread created by the Form does not need to access the Office object model, but does need to update a control on the Windows Form (the Form was created by the addin or 'UI' thread as I sometimes hear it referred to)
For 1.) above I've taken the following https://msdn.microsoft.com/en-us/library/8sesy69e%28v=vs.120%29.aspx to mean that it is safe as long as you set the thread's apartment state to STA and you handle exceptions. But http://weblogs.asp.net/whaggard/all-outlook-object-model-calls-run-on-the-main-thread seems to imply that in .NET VSTO, calls to the object model from any background thread are safe as they're automatically marshalled to the main thread for you, and making the 'background' thread STA is merely for performance reasons. Is that all there is to it?
For 2.) Is there any problem with letting the 'thread' be a Task or a IsBackground thread, provided it uses the control's InvokeRequired/Invoke pattern? Or does it need to be a STA thread doing the Invoking?
Update I've seen several VSTO experts mention not to touch the Outlook object model on anything other than the main thread and that in Outlook 2013 it will actually throw an error if you do it. I have an add-in that does in fact access the Outlook object model on a couple background threads (system.timers.timer, background thread) and I hadn't seen such errors in my log. Then all of a sudden a couple days ago, there was a span of about 10 minutes where my addin's error log was full of the following errors:
The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))
After this 10 minute span, the errors mysteriously disappeared, and I hadn't made any changes to the code, both immediately prior to the errors and immediately after the errors. Prior to that, I'd been running the addin for months on my machine (primarily in cached connection mode, if it matters) without seeing any such errors.
I'd love if someone could point me to the Microsoft documentation where it says not to access the object model on background threads.
Solution 1:[1]
Calls to Outlook Object Model on secondary threads are definitely not supported. OOM objects can hold references to UI controls, which have thread affinity. In older versions of Outlook, some objects used to work, some failed, but starting in Outlook 2016, as soon as Outlook detects access on a thread different from the main one, it throws an exception. Keep in mind that this applies only when running inside the outlook.exe address space (COM addins including VSTO) - external calls to OOM are always marshaled to the main Outlook thread, so using a secondary thread in an external process will end up on the main Outlook thread anyway.
Pretty much the only workaround if you just have to use secondary threads (e.g. if you are accessing network or external databases etc.) is to marshal OOM related calls to the main thread using Dispatcher.Invoke()
(where Dispatcher
object is retrieved from Dispatcher.CurrentDispatcher
on the main thread). Since the marshaling calls are expensive (your secondary thread stops and the switch to the main thread occurs when Outlook is idling and enters Windows message loop), try to batch multiple OOM calls into a single Dispatcher.Invoke()
- don't use it for each and every OOM call on your secondary thread.
Extended MAPI objects (e.g. IMessage
retrieved from MailItem.MAPIOBJECT
or IMAPISession
from Namespace.MAPIOBJECT
) are thread-safe, but Extended MAPI is supported in native languages like C++ or Delphi only, not in .Net. Also remember that each thread that uses MAPI must call MAPIInitialize
before any MAPI calls are made on that thread and MAPIUninitialize
must be called when the thread is shut down.
If Extended MAPI in C++ or Delphi is not an option, you can use the Redemption library (I am its author) - it is an Extended MAPI wrapper and can be used from .Net languages. You can save the value of Namespace.MAPIOBJECT
(which is IMAPISession
) in a dedicated variable (this way only the MAPI object gets marshaled by .Net) on the main thread, then on the secondary thread create a new instance of the RDOSession
object (that will initialize MAPI on that thread by calling MAPIInitialize
) and set the RDOSession.MAPIOBJECT
property to the value saved from OOM on the main thread - this way the secondary thread will share the same MAPI session used by Outlook itself. It is a good idea to release all intermediary objects on that thread (including RDOSession
) by calling Marshal.ReleaseComObject
- this way the object is deterministically released on that thread rather than on a thread owned by the .Net GC.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |