COMAddIns Race Condition
You can expose an arbitrary object from your add-in as a kind of extension to the Office host application’s object model. To do this, you set your object as the value of the Object property on the COMAddIn object that represents your add-in in the host’s COMAddIns collection object. You can do this directly in a non-VSTO add-in, or through VSTO’s RequestComAddInAutomationService mechanism.
If you intend for this to be used by other add-ins, VSTO doc-level customizations, VBA, or any other in-proc extension (smart tag, realtime data component, etc), the race condition does not apply. However, once you’ve exposed your object, it is also callable from out-of-proc automation clients – whether this is your intention or not.
For out-of-proc clients, there is a race condition on startup. Specifically, there’s a window of opportunity as the host application is starting up where such clients can get hold of the COMAddIns collection, and individual COMAddIn objects, before add-ins have actually been loaded.
For example, let’s suppose I have an Excel add-in (called “ComServiceOleMarshal”) that exposes an AddinUtilities object:
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IAddinUtilities
{
void DoSomething();
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class AddinUtilities : IAddinUtilities
{
public void DoSomething()
{
MessageBox.Show("Something");
}
}
public partial class ThisAddIn
{
private AddinUtilities addinUtilities;
protected override object RequestComAddInAutomationService()
{
if (addinUtilities == null)
{
addinUtilities = new AddinUtilities();
}
return addinUtilities;
}
}
…and a WPF client that offers a Button. When the user clicks the Button, we’ll launch Excel, get hold of the add-in and its AddinUtilities object, and invoke the DoSomething method:
private void buttonInvokeAddIn_Click(object sender, RoutedEventArgs e)
{
Excel.Application excel = null;
try
{
excel = new Excel.Application();
excel.Visible = true;
object addinName = "ComServiceOleMarshal";
Office.COMAddIn addin = excel.COMAddIns.Item(ref addinName);
ComServiceOleMarshal.IAddinUtilities utils = null;
utils = (ComServiceOleMarshal.IAddinUtilities)addin.Object;
utils.DoSomething();
MessageBox.Show("Close this message to continue");
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
finally
{
if (excel != null)
{
excel.Quit();
excel = null;
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
Nine times out of 10 (or, actually, more like 9,999 times out of 10,000), this will fail on the line that attempts to invoke the DoSomething method. This is because the previous line failed to get hold of the AddinUtilities object and returned null into the utils variable.
It would be nice if there were some convenient event fired by the host application to notify interested listeners when the add-ins have actually been loaded, and the COMAddIns collection contains fully-populated objects. Unfortunately, there is no such event. For Excel, you might consider sinking events such as the WorkbookActivate, WorkbookOpen or WindowActivate events. Similar events are exposed by the other Office apps. Unfortunately, these will not fire in all scenarios, and typically will not fire when the application first starts.
Instead, you might think you could put in an artificial delay in your application: perhaps you could launch Excel and get the COMAddIn object when your window is loaded, and then implement your Button handler to fetch the AddinUtilities object later. Unfortunately, while this might reduce the chances that you’ll hit the race condition (simply by virtue of the fact that you’re doing extra work before attempting to get the object), it doesn’t solve it, and is non-deterministic.
So, the only sensible approach is to attempt to get the AddinUtilities object, and retry until you succeed. This can actually be pretty lightweight:
while (utils == null)
{
utils = (ComServiceOleMarshal.IAddinUtilities)addin.Object;
System.Threading.Thread.Sleep(100);
}
utils.DoSomething();
You’d probably also want to build in some bail-out clause, so that you don’t end up retrying forever. For example, you could prompt the user, or simply restrict the number of times to retry, and so on. Here’s a simple example:
while (utils == null)
{
if (MessageBox.Show("Continue trying to get the object?",
"Alert", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
System.Threading.Thread.Sleep(100);
utils = (ComServiceOleMarshal.IAddinUtilities)addin.Object;
}
else
{
break;
}
}
if (utils != null)
{
utils.DoSomething();
}
Update: turns out this is a bug in the VSTO runtime - to be fixed soon, I hope.
ComServiceOleMarshal_RaceCondition.zip
Comments
- Anonymous
August 15, 2008
PingBack from http://informationsfunnywallpaper.cn/?p=657 - Anonymous
October 13, 2008
Following on from my recent posts on exposing add-in objects, here and here , it occurred to me that