具有 Windows 窗体和辅助线程的 VSTO

VSTO with Windows Form and Worker Threads

我有一个使用 VSTO 创建的 Office 插件(如果有影响,请使用 Outlook)。插件在启动时创建一个 Windows 表单。我的表单创建了多个线程。我正在寻找具体指导或人们在以下情况下安全工作的经验:

1.) Form创建的线程需要访问Office对象模型(Globals.ThisAddIn.Application)

2.) Form 创建的线程不需要访问 Office 对象模型,但需要更新 Windows Form 上的控件(该 Form 是由 addin 或 'UI' 线程,因为我有时会听到它)

对于 1.) 以上,我采取了以下 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 似乎暗示在 .NET VSTO 中,从任何后台线程调用对象模型都是安全的,因为它们会自动编组到主线程为您创建线程,而使 'background' 线程成为 STA 仅仅是出于性能原因。仅此而已吗?

对于 2.) 让 'thread' 成为任务或 IsBackground 线程是否有任何问题,前提是它使用控件的 InvokeRequired/Invoke 模式?或者它是否需要是一个 STA 线程来执行调用?

更新 我见过几位 VSTO 专家提到不要在主线程以外的任何地方接触 Outlook 对象模型,在 Outlook 2013 中,如果你这样做,它实际上会抛出错误。我有一个加载项,它实际上在几个后台线程(system.timers.timer、后台线程)上访问 Outlook 对象模型,而且我没有在我的日志中看到此类错误。然后几天前突然间,我的插件的错误日志中有大约 10 分钟的时间跨度充满了以下错误:

The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))

在这 10 分钟之后,错误神秘地消失了,我没有对代码进行任何更改,无论是在错误之前还是错误之后。在此之前,我已经 运行 在我的机器上使用了几个月的插件(主要是缓存连接模式,如果重要的话),但没有看到任何此类错误。

如果有人能给我指点 Microsoft 文档,我会很乐意看到它说不要在后台线程上访问对象模型。

绝对不支持在辅助线程上调用 Outlook 对象模型。 OOM 对象可以保存对 UI 控件的引用,这些控件具有线程关联性。在旧版本的 Outlook 中,一些对象曾经工作,一些失败,但从 Outlook 2016 开始,一旦 Outlook 检测到在与主线程不同的线程上进行访问,它就会引发异常。请记住,这仅适用于 outlook.exe 地址 space 内的 运行(包括 VSTO 的 COM 插件)- 对 OOM 的外部调用始终编组到主 Outlook 线程,因此使用辅助线程无论如何,外部进程中的线程将在主 Outlook 线程中结束。

如果您只需要使用辅助线程(例如,如果您正在访问网络或外部数据库等),几乎唯一的解决方法是使用 Dispatcher.Invoke() 将与 OOM 相关的调用编组到主线程(其中 Dispatcher 对象在主线程上从 Dispatcher.CurrentDispatcher 中检索)。由于编组调用很昂贵(当 Outlook 空闲并进入 Windows 消息循环时,您的辅助线程停止并切换到主线程),请尝试将多个 OOM 调用批处理为单个 Dispatcher.Invoke() - 不要'将它用于辅助线程上的每个 OOM 调用。

扩展 MAPI 对象(例如 IMessageMailItem.MAPIOBJECT 检索或 IMAPISessionNamespace.MAPIOBJECT 检索)是线程安全的,但扩展 MAPI 在 C++ 等本机语言中受支持或仅 Delphi,不在 .Net 中。还请记住,每个使用 MAPI 的线程必须在对该线程进行任何 MAPI 调用之前调用 MAPIInitialize,并且当线程关闭时必须调用 MAPIUninitialize

如果 C++ 中的扩展 MAPI 或 Delphi 不是一个选项,您可以使用 Redemption 库(我是它的作者)——它是一个扩展 MAPI 包装器,可以从 .网络语言。您可以在主线程上将 Namespace.MAPIOBJECT(即 IMAPISession)的值保存在专用变量中(这样只有 MAPI 对象被 .Net 封送处理),然后在辅助线程上创建一个新的RDOSession 对象的实例(将通过调用 MAPIInitialize 在该线程上初始化 MAPI)并将 RDOSession.MAPIOBJECT 属性 设置为从主线程上的 OOM 保存的值 - 这个辅助线程将共享 Outlook 本身使用的相同 MAPI 会话的方式。通过调用 Marshal.ReleaseComObject 释放该线程(包括 RDOSession)上的所有中间对象是个好主意 - 这样对象就确定性地在该线程上释放,而不是在 .Net GC 拥有的线程上释放.