다음을 통해 공유


Non-Cocreatable Class Objects No Longer Wrapped in NDP 2.0 and Later

I dealt with a customer attempting to automate Microsoft Outlook 2003 using the Outlook Primary Interoperability Assembly (Microsoft.Office.Interop.Outlook.dll) for the Microsoft Outlook 11.0 Object Library type library version 9.2.

The type library contains the following items:

[…noncreatable…]

coclass MailItem {

    [default] interface _MailItem;

    [source] dispinterface ItemEvents;

    [default, source] dispinterface ItemEvents_10;

};

interface _MailItem : IDispatch {

};

dispinterface ItemEvents {

};

dispinterface ItemEvents_10 {

};

As you can see, the type library contains the noncreatable coclass MailItem which implements one interface and supports two eventing interfaces (with ItemEvents_10 being the default).

The primary interoperability assembly for the type library contains:

interface Microsoft.Office.Interop.Outlook._MailItem

interface Microsoft.Office.Interop.Outlook.ItemEvents_Event

interface Microsoft.Office.Interop.Outlook.ItemEvents_10_Event

interface Microsoft.Office.Interop.Outlook.MailItem

      implements Microsoft.Office.Interop.Outlook._MailItem

      implements Microsoft.Office.Interop.Outlook.ItemEvents_10_Event

class Microsoft.Office.Interop.Outlook.MailItemClass

      implements Microsoft.Office.Interop.Outlook._MailItem

      implements Microsoft.Office.Interop.Outlook.MailItem

      implements Microsoft.Office.Interop.Outlook.ItemEvents_10_Event

      implements Microsoft.Office.Interop.Outlook.ItemEvents_Event

As you can see, the MailItem interface implements just the default interface and the default event interface whereas the MailtItemClass implements every interface. Note that the customer can get to the non-default event interface from a MailItem interface by casting it to an ItemEvents_Event interface.

The customer has code like:

 

Outlook.Explorer oExp = …

                 

    Outlook.Selection oSel = oExp.Selection;

    if (oSel[1] is Outlook.MailItem)

    {

        //Outlook.MailItem MI =

        // (Outlook.MailItem) oSel[i]; ß This works

        Outlook.MailItemClass mailItem =

            (Outlook.MailItemClass)oSel[1]; ß This throws exception

    }

When run, the previously listed code throws this exception:

Unhandled Exception: System.InvalidCastException: Unable to cast COM object of type 'System.__ComObject' to class type 'Microsoft.Office.Interop.Outlook.MailItemClass'. COM components that enter the CLR and do not support IProvideClassInfo or that do not have any interop assembly registered will be wrapped in the __ComObject type. Instances of this type cannot be cast to any other class; however they can be cast to interfaces as long as the underlying COM component supports QueryInterface calls for the IID of the interface.

at Repro.Main()

The object returned from oExp.Selection is being bound to the type __ComObject instead of the type MailItemClass. Note that it can still be cast to all of the interfaces, but the cast to the class will fail. This code runs without the exception on pre-v2.0 versions of the CLR.

Explanation of Behavior

When a Runtime Callable Wrapper (RCW) is being created for a COM object, the runtime QueryInterfaces the COM object for IProvideClassInfo to determine if the interface is a coclass. If it is a coclass, the CLSID is fetched for the object and the runtime attempts to bind the wrapper object’s type to the typed class wrapper instead of a generic __ComObject.

The system has a hashtable mapping CLSID’s to the appropriate class wrapper types. The normal way that this table is populated is to look in the registry under:

HKEY_CLASSES_ROOT\CLSID\{CLSID}\InprocServer32

      Assembly ß Strong name of assembly that contains class wrapper

      Class ß Name of the wrapper class

The problem with this is that there are no registry entries for noncreatable coclasses (such as MailItem).

The reason that this used to work pre-v2.0 is because there used to be an optimization in this process that could lead to non-deterministic behavior.

Each Runtime Callable Wrapper (RCW) class that wraps a coclass provides information about the CLSID of the coclass that it wraps. When such a class was loaded in pre-v2.0 versions of the runtime, the CLSID to class mapping was added to the table if it was not already present.

This meant that non-cocreatable COM objects would map to __ComObject prior to loading the correct wrapper class (if the mapping didn’t exist in the registry), but COM objects of the same type would bind to the typed wrapper class after an action had caused the appropriate wrapper class to be loaded once.

Because of the non-determinism of this behavior, this optimization was removed in v2.0.

Solution to the problem

The correct solution is for the customer to not use the coclass wrapper class for noncreatable coclass’s. The customer can cast the interface that they have to any other interface (or event interface) to access to all of the functionality exposed by the coclass wrapper class.

The coclass wrapper class is a flattening of all of the methods of all of the interfaces (including eventing interfaces) implemented by the given COM object. Although simple to use, casting to interfaces gives the programmer access to all of the functionality exposed by the COM object.

I would go so far as to recommend everyone program against the actual interfaces instead of using the flattened typed class object. I think that trying to hide the interface based nature of COM was a bad idea. I feel like other issues caused by this flattening and hiding of interfaces could possibly introduce more breaking changes like this in future releases (note that I said possibly).

This post does bring up the whole topic of the __ComObject. People get very confused and think something is wrong when they see this for the type of a runtime callable wrapper in the debugger. I’ll try to tackle a more general discussion of RCWs and the nature of __ComObject in a future post.

Comments

  • Anonymous
    April 18, 2007
    The term COM object is frequently thrown around, but people really access interfaces off of objects.

  • Anonymous
    May 31, 2007
    If you have seen the error below when using objects from OOM in .NET 2.0 code then the information that

  • Anonymous
    June 12, 2007
    The comment has been removed

  • Anonymous
    July 24, 2007
    The comment has been removed

  • Anonymous
    January 13, 2008
    We have an Outlook Add-in written in .NET 1.1 that raises exactly the exception mentioned above. The reason is that we use the MailItemClass rather than MailItem. We need to do this is we wish to capture the ItemSend event. Even if we try to add the handler explicitly, i.e.    AddHandler m_olMailItem.ItemSend, AddressOf m_olMailItem_ItemSend we get the exception in the routine - despite the fact that we do not cast to a mailitemclass. Any details on how we should leverage events exposed in MailItemClass would be great.