Search code examples
vb.nettypescomwindows-shelltypename

Why does TypeName() return different results from .GetType and TypeOf when working with COM?


I feel like I would benefit greatly from understanding the differences in how these functions work so that I could better understand when to use each one.

I'm having a very difficult time working with two different interops (Excel, and EPDM) which have both made extensive use of weak typed parameters. I keep running into problems using returned objects and casting them to the proper type (per the documentation). After wasting a ton of time, I've found that using TypeName, GetType, and a TypeOf operator with COM objects can yield different results, and in different circumstances each one can be more or less reliable than the next.

Now, in most cases TypeName() seems to be the most reliable for determining type with COM objects. However, avoiding the other two functions entirely seems quite cargo cultish to me, and besides that today I ran into an interesting problem where I can't seem to cast an object to the type reported by TypeName(). An interesting notion was brought up in the comments on that problem that objects which implement IDispatch may actually return the dispatched interface typename, which could partially explain the differences.

I'd really like to better understand how these functions actually work, but I get kind of lost running through the .NET ReferenceSource, so I'm offering a bounty on this question in hopes someone can explain how these different functions work and in what context each should be used.

Here is a code excerpt from working with the Excel interop.

Dim DocProps As Object 
DocProps = WeeklyReports.CustomDocumentProperties 'WeeklyReports is a Workbook object
Debug.Print(DocProps Is Nothing)
Debug.Print(TypeName(DocProps))
Debug.Print(TypeOf (DocProps) Is DocumentProperties)
Debug.Print(DocProps.GetType.ToString)

The output is:

False
DocumentProperties
False
System.__ComObject


Solution

  • It is a long story and a bit doubtful that English is going to cut it. It does require understanding how COM works and how it was integrated into the Office products.

    At breakneck speed, COM is very heavily an interface-based programming paradigm at its core. Interfaces are easy, classes are hard. Something you see back in the .NET design as well, a class can derive from only one single base class but can implement any number of interfaces. To make language interop work smoothly, it is important to take as few dependencies on language implementation details as possible.

    There is a lot that COM does not do that you'd be used to in any modern language. It does not support exceptions, only error codes. No notion of generics at all. No Reflection. No support for method overloads. No support for implementation inheritance whatsoever, the notion of a class is completely hidden. It only appears as a number, the CLSID, a guid that identifies a class type. With a factory function implemented in the COM component that creates an object of the class. The COM component retains ownership of that object. The client code then only ever uses interfaces to make calls to use methods and get or set properties. CoCreateInstance() is the primary runtime support function that does this.

    This was further whittled down to a subset called OLE Automation, the flavor that you use when you interop with Office. It strictly limits the kind of types you can use for properties and method arguments with prescribed ways to deal with the difficult ones like strings and arrays. It does add some capabilities, it supports late binding through the IDispatch interface, important to scripting languages. And VARIANTs, a data type that can store a value or object reference of an arbitrary type. And supports type libraries, a machine-readable description of the interfaces implemented by the COM server. .NET metadata is the exact analogue.

    And important to this question, it limit the number of interfaces that a class can implement to just one. Important to languages that don't support the notion of interfaces at all, like VBA, scripting languages like Javascript and VBScript and early Visual Basic versions. The Office interop object model was very much designed with these limitations in mind.

    So from the point of view from a programmer that uses such a language to automate an Office program, it is completely invisible that his language runtime is actually using interfaces. All he ever sees and uses in his program are identifiers that look like class names, not interface names. That DocumentProperties is actually an interface name is something you can see in Object Browser. Just type the name in the search box, it properly annotates "public interface DocumentProperties / Member of Microsoft.Office.Core" in the lower-right panel.

    One specific detail of the Office object model matters a great deal here, many properties and method return types are VARIANTs. A OLE Automation type that can store an arbitrary value or object reference, it is mapped to System.Object when you use .NET. The Workbook.CustomDocumentProperties property is like that. Even though the property is documented to actually return a DocumentProperties interface reference. They probably did this to leave elbow room to some day return another kind of interface. Fairly necessary for "custom document properties".

    That the property is a VARIANT doesn't matter that much in languages that support dynamic typing, they take them with stride. It is however pretty painful in a strongly typed language. And pretty unfriendly to programming editors that support auto-completion, like VS's IntelliSense. What you normally do is declare your variable to the expected interface type:

      Dim DocProps As DocumentProperties
      DocProps = CType(WeeklyReports.CustomDocumentProperties, Microsoft.Office.Core.DocumentProperties)
    

    And now everything lights up. You don't need the CType() cast either if you favor programming VB.NET with Option Strict Off in effect. Which turns it into a programming language that supports dynamic typing well.


    We're getting there. As long as you declare DocProps as Object then the compiler knows beans about the interface. Nor does the debugger, it isn't helped by the variable declaration and can only see that it is a __System.ComObject from the runtime type. So it isn't Nothing, that's easy enough to understand, the property getter did not fail and the document has properties.

    The TypeName() function uses a feature of the IDispatch interface, it exposes type information at runtime. That happens to work in your case, it usually doesn't, the function first calls IDispatch::GetTypeInfo() to get an ITypeInfo interface reference, then calls ITypeLib::GetDocumentation(). That works, you get the interface name back. Otherwise pretty comparable to Reflection in .NET, just not nearly as powerful. Do not rely on it heavily, there are lots of COM components that don't implement this.

    And crucial to your question, TypeOf (DocProps) Is DocumentProperties is a fail whale. Something you'll discover when you try to write the code I proposed earlier. You'll get a nasty runtime exception, System.InvalidCastException:

    {"Unable to cast COM object of type 'System.__ComObject' to interface type 'Microsoft.Office.Core.DocumentProperties'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{2DF8D04D-5BFA-101B-BDE5-00AA0044DE52}' failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE))."}

    In other words, the Excel documentation is lying to you. You get an interface back that resembles DocumentProperties, it still has the members that this interface documents, but is no longer identical to the Microsoft.Office.Core.DocumentProperties. It probably once was, many moons ago. A nasty little detail that's buried inside this KB article:

    Note The DocumentProperties and the DocumentProperty interfaces are late bound interfaces. To use these interfaces, you must treat them like you would an IDispatch interface.