共用方式為


Why your COMAddIn.Object should derive from StandardOleMarshalObject

In general, it is important that any code in a managed Office add-in should execute on the main UI thread. The reason for this is that there are several components that simply will not work when executed from any other but the main UI thread – examples include calls on WinForms controls, and calls into the VSTO runtime. Fortunately, in most normal circumstances, your add-in code will run on the main UI thread.

However, there is one scenario where you need to be more vigilant. Recall that you can expose your add-in through the COMAddIn.Object property of the COMAddIn object that represents your add-in in the COMAddIns collection, either directly (in a non-VSTO add-in) or through VSTO’s RequestComAddInAutomationService mechanism (documented here). If this object is consumed in-proc (for example, by another add-in in the same process, or a VSTO doc-level customization, or VBA macro), there should be no problem. On the other hand, if the object is consumed by an out-of-proc client, there will be a problem.

By default, calls coming from out-of-proc clients will not be executed on the main UI thread. This is because from COM’s perspective all managed objects by default are thread-neutral. One consequence of this is that COM will not attempt to switch threads to execute calls into such objects, and since cross-process COM calls will come in on an arbitrary RPC thread, COM decides to make the call right on this thread.

This, of course, is not the behavior that we want. What we really want is to tell COM that our managed object lives in a Single Threaded Apartment (STA), because then COM will marshal the call onto the thread the object was created on (which, for Office add-ins, will be the main UI thread). To achieve this, you can simply derive your exposed add-in object from System.Runtime.InteropServices.StandardOleMarshalObject. This class is documented here.

The underlying reasons for this behavior go back to the original design of COM and OLE (OLE being a layer on top of COM that supports various UI mechansims). Essentially, COM objects involved with UI programming nearly always require a single-threaded apartment. Multi-threaded apartments were designed on the assumption that there was no UI. Further details here.

For an example of where using StandardOleMarshalObject will make a difference, 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()

    {

        Globals.ThisAddIn.CreateNewTaskPane();

    }

}

The main ThisAddIn object in the add-in instantiates this AddinUtilities object, exposes it via RequestComAddInAutomationService, and also exposes a CreateNewTaskPane method which the AddinUtilities object calls. The CreateNewTaskPane method uses the VSTO CustomTaskPanes.Add method, which internally invokes Office code in order to create a new task pane. Thus, we’ve set things up such that any call into the AddinUtilities method will result in a call into the Office host app.

public partial class ThisAddIn

{

    private AddinUtilities addinUtilities;

    protected override object RequestComAddInAutomationService()

    {

        if (addinUtilities == null)

        {

            addinUtilities = new AddinUtilities();

        }

        return addinUtilities;

    }

    internal void CreateNewTaskPane()

    {

        Microsoft.Office.Tools.CustomTaskPane taskPane =

            this.CustomTaskPanes.Add(new UserControl(), "New TaskPane");

        taskPane.Visible = true;

    }

}

If you write client code to consume this AddinUtilities object in an in-proc component, such as another Excel add-in, a VSTO doc-level customization, or from VBA, everything should work OK:

'In-proc VBA

Private Sub CommandButton1_Click()

    Dim addin As Office.COMAddIn

    Dim automationObject As Object

    Set addin = Application.COMAddIns("ComServiceOleMarshal")

    Set automationObject = addin.Object

    automationObject.DoSomething

End Sub

However, if you write an external automation client to invoke the AddinUtilities object, things will go horribly wrong. For example, here’s a simple managed console client:

static void Main(string[] args)

{

    Excel.Application excel = new Excel.Application();

    excel.Visible = true;

    Console.WriteLine("press a key to invoke the AddinUtilities object");

    Console.ReadLine();

    object addinName = "ComServiceOleMarshal";

    Office.COMAddIn addin = excel.COMAddIns.Item(ref addinName);

    ComServiceOleMarshal.IAddinUtilities utils =
(ComServiceOleMarshal.IAddinUtilities)addin.Object;

    utils.DoSomething();

    Console.WriteLine("press a key to close Excel");

    Console.ReadLine();

}

When this code attempts to call through to the AddinUtilities object, we’ll typically get an E_NOINTERFACE error like this:

System.InvalidCastException: Unable to cast COM object of type 'System.__ComObject' to interface type 'Microsoft.VisualStudio.Tools.Office.Runtime.Interop.ICustomTaskPaneSite'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{3CA8CD11-274A-41B6-A999-28562DAB3AA2}'failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)). at ComServiceOleMarshal.IAddinUtilities.DoSomething() at ConsoleTest.Program.Main(String[] args) in C:\Temp\ComServiceOleMarshal\ConsoleTest\Program.cs:line 29

This message is pretty obscure. What’s happening is that the CustomTaskPanes.Add method uses the ICustomTaskPaneSite interface to call into the VSTO runtime. Since the VSTO runtime is implemented as an STA COM object, the call has to go through the COM marshaling mechanism. However, the ICustomTaskPaneSite interface has not been designed to be marshaled, and so the entire call fails. To fix all this, you can simply derive the AddinUtilities class from StandardOleMarshalObject, and rebuild:

[ComVisible(true)]

[ClassInterface(ClassInterfaceType.None)]

public class AddinUtilities :

    StandardOleMarshalObject,

    IAddinUtilities

{

    public void DoSomething()

    {

        Globals.ThisAddIn.CreateNewTaskPane();

    }

}

Now calls to the exposed object should work correctly for out-of-proc clients also. Source code for the add-in and the client are attached to this post. Many thanks to Misha for helping me figure out the exact behavior here.

ComServiceOleMarshal_WpfClient.zip

Comments

  • Anonymous
    August 11, 2008
    PingBack from http://blog.a-foton.ru/2008/08/why-your-comaddinobject-should-derive-from-standardolemarshalobject/
  • Anonymous
    August 11, 2008
    The COMAddIns property is a collection of COMAddIn objects exposed by Office applications that support
  • Anonymous
    October 13, 2008
    Following on from my recent posts on exposing add-in objects, here and here , it occurred to me that
  • Anonymous
    November 18, 2008
    I am getting the exception"System.Runtime.Remoting.RemotingException" during the call   utils.DoSomething();Am I missing anything.
  • Anonymous
    November 18, 2008
    The comment has been removed
  • Anonymous
    November 20, 2008
    I'm using your example to try to pass an object to the AddinUtilities class. AddinUtilities it's inheriting from StandardOleMarshalObject and implementing IAddinUtilities. If I pass an integer value it works fine, but as soon as I try to pass an object, either by value or by reference, I get an Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).Is it possible to pass objects or am I trying something impossible?
  • Anonymous
    December 01, 2008
    Mariano - you can make your object type serializable - see my new post here: http://blogs.msdn.com/andreww/archive/2008/11/30/passing-objects-to-exposed-add-in-methods.aspx
  • Anonymous
    December 18, 2008
    Hi Andrew,If I use "StandardOleMarshalObject" attribute, then I get the following exception in my C# win form program."This remoting proxy has no channel sink which means either the server has no registered server channels that are listening, or this application has no suitable client channel to talk to the server"But If do not use "StandardOleMarshalObject" then everything works fine.
  • Anonymous
    March 09, 2009
    The comment has been removed
  • Anonymous
    March 13, 2009
    Alberto - this should work. I don't see anything in the code snippets you've posted that would show any problems. Can you post the rest of the relevant code for your add-in and client?
  • Anonymous
    June 02, 2009
    I have the same problem as Alberto.  The funny thing is that the IID {...} that is says is not defined in QueryInterface  is looking for does not exist anywhere in my Registry or in either the AddIn or in the external .NET app.  Seems like a lot of people are having this problem.
  • Anonymous
    June 10, 2009
    Bill - same question as to Albert: there's not enough information in the comment to indicate what the problem might be. Can you post the code for the simplest repro of this?
  • Anonymous
    August 08, 2009
    Bill, you have to set your add-in project's Build settings to "Register for COM Interop". You do not need to set the 'make assembly com visible' as the decorations take care of the needed interfaces/classes.Got this from the comments on one of Andrew's (excellent) earlier posts. http://blogs.msdn.com/andreww/comments/1473949.aspx
  • Anonymous
    August 10, 2009
    Hi, AndrewI am trying to send email from wpf window using outlook 2003 addin ( not 2007). I am using your template.But in WPF project/Window.xaml.cs, it seems the mailer is always==null, so I hit the endless while loop. You help would be really appreciated.Thank you in advanceHere is the code:private void Send_Click(object sender, RoutedEventArgs e)       {           Outlook.Application outlook = null;          try           {               outlook = new Outlook.Application();               object addinName = "Outlook2003AddIn";               Office.COMAddIn addin = outlook.COMAddIns.Item(ref addinName);               Outlook2003AddIn.IMailer mailer= null;               while (mailer == null)                   {                       mailer = (Outlook2003AddIn.IMailer)addin.Object;                       System.Threading.Thread.Sleep(100);                   }              mailer.SendEmail("Lynn.lin@sjrb.ca", "Hello", "Test", "Lynn.lin@sjrb.ca");               MessageBox.Show("close this message to continue");           }           catch (Exception ex)           {               MessageBox.Show(ex.ToString());           }           finally           {               if (outlook != null)               {                   outlook.Quit();                   outlook = null;               }               GC.Collect();               GC.WaitForPendingFinalizers();               GC.Collect();               GC.WaitForPendingFinalizers();           }       }In Outlook2003AddIn Project:using System;using System.Threading;using Outlook = Microsoft.Office.Interop.Outlook;using Office = Microsoft.Office.Core;namespace Outlook2003AddIn{   public partial class ThisAddIn   {       private Mailer mailer;       protected override object RequestComAddInAutomationService()       {           if(mailer==null)           {               mailer=new Mailer();               mailer.threadId = Thread.CurrentThread.ManagedThreadId;           }           return mailer;       }       internal void SendEmail(string toValue, string subjectValue, string bodyValue,                               string fromAddress)       {           var _oApp = Globals.ThisAddIn.Application;           var _oNameSpace = _oApp.GetNamespace("MAPI");           _oNameSpace.Logon(null, null, true, true);           var _oSentTItemsFolder = _oNameSpace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail);           var oMailItem =                (Outlook._MailItem)_oApp.CreateItem(Outlook.OlItemType.olMailItem);           try           {               oMailItem.To = toValue;               oMailItem.Subject = subjectValue;               oMailItem.Body = bodyValue;                var iPosition = bodyValue.Length + 1;                   var iAttachType = (int)Outlook.OlAttachmentType.olByValue;               oMailItem.SaveSentMessageFolder = _oSentTItemsFolder;               oMailItem.Send();           }           catch (Exception ex)           {               // LogHelper.WriteError(this, "SendEmail Error", ex);           }       }       private void ThisAddIn_Startup(object sender, System.EventArgs e)       {       }       private void ThisAddIn_Shutdown(object sender, System.EventArgs e)       {       }       #region VSTO generated code       /// <summary>       /// Required method for Designer support - do not modify       /// the contents of this method with the code editor.       /// </summary>       private void InternalStartup()       {           this.Startup += new System.EventHandler(ThisAddIn_Startup);           this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);       }       #endregion   }}using System.Diagnostics;using System.Runtime.InteropServices;using System.Threading;namespace Outlook2003AddIn{   [ComVisible(true)]   [ClassInterface(ClassInterfaceType.None)]   public class Mailer :StandardOleMarshalObject,IMailer   {       public int threadId;       public void SendEmail(string toValue, string subjectValue, string bodyValue,                             string fromAddress)       {           int id = Thread.CurrentThread.ManagedThreadId;           if (id == this.threadId)           {               Debug.WriteLine("Same thread");           }           else           {               Debug.WriteLine("Different thread");           }           Globals.ThisAddIn.SendEmail(toValue, subjectValue, bodyValue, fromAddress);   }   [ComVisible(true)]   [InterfaceType(ComInterfaceType.InterfaceIsDual)]   public interface IMailer   {       void SendEmail(string toValue, string subjectValue, string bodyValue,                      string fromAddress);   }}