MSHTML HTMLHeadElementClass COM Error

2019-02-15 22:20发布

问题:

I'm currently working with MS HTML to insert a JavaScript code into websites.

I made a reference to Microsoft HTML Object Library and types this code.

IHTMLDocument2 doc = BrowserHost.Document as HTMLDocumentClass;
IHTMLElement head = (IHTMLElement)
      ((IHTMLElementCollection)doc.all.tags("head")).item(null, 0);
IHTMLScriptElement scriptObject = 
      (IHTMLScriptElement)doc.createElement("script");
scriptObject.type = @"text/javascript";
scriptObject.text = TTS.TTSWebFactory.GetJavascript();
((HTMLHeadElementClass)head).appendChild((IHTMLDOMNode)scriptObject);

I get an error on the last line of the script, this is the message.

Unable to cast COM object of type 'System._ComObject' to class type 
'mshtml.HTMLHeadElementClass'. COM components that enter the CLR and do not 
support IProvideClassInfo or that do not havae any iterop 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

I don't have much experience with COM and it is important to keep the last line int he code, can any one help me understand what this means and how i solve it ?

回答1:

There is a very serious problem with the interop classes that you get out of a type library like mshtml.tlb. The type library importer generates synthetic .NET classes from the coclass definitions in the type library. You can recognize them from their type names, they end in "Class" and start with the name of an interface type. They are meant to be helpful, allowing you to treat the COM coclass as though it was a .NET class.

They however cause a very serious versioning problem. When I use oleview.exe to look at mshtml.tlb on my machine then I see this definition for the HTMLHeadElement coclass:

[
  uuid(3050F493-98B5-11CF-BB82-00AA00BDCE0B),
  noncreatable
]
coclass HTMLHeadElement {
    [default] dispinterface DispHTMLHeadElement;
    [default, source] dispinterface HTMLElementEvents;
    [source] dispinterface HTMLElementEvents2;
    interface IHTMLElement;
    interface IHTMLElement2;
    interface IHTMLElement3;
    interface IHTMLElement4;
    interface IHTMLUniqueName;
    interface IHTMLDOMNode;
    interface IHTMLDOMNode2;
    interface IHTMLElement5;
    interface IHTMLDOMConstructor;
    interface IHTMLHeadElement;
    interface IHTMLHeadElement2;
};

When I right-click mshtml.HtmlHeadElementClass in the source code and select Go To Definition then I see this:

public class HTMLHeadElementClass : DispHTMLHeadElement, HTMLHeadElement, 
   HTMLElementEvents_Event, IHTMLElement, IHTMLElement2, IHTMLElement3, 
   IHTMLElement4, IHTMLUniqueName, IHTMLDOMNode, IHTMLDOMNode2, 
   IHTMLHeadElement, IHTMLElementEvents2_Event {
   // Lots of members
   //...
}

Note the mismatch between the two. The one I got from Oleview has extra interfaces, IHtmlElement5 and IHtmlHeadElement2. The reason is simple to explain, I have IE9 installed on my machine. The interop definition came from the mshtml PIA. Which is an old version, probably dating back to IE7.

The horrifying problem is that new IHtmlElement5 interface got inserted into the inheritance list. This was a very serious mistake, at least as far as .NET code goes. It doesn't matter at all to pure COM code, that only ever works with interfaces and asks the coclass for an interface pointer with QueryInterface. It is however fatal to the .NET wrapper class, all of the methods past IHTMLDOMNode2 have the wrong offset. So if you call the IHTMLHeadElement.appendChild method then you'll end up calling the wrong method.

This is an unsolvable problem. You can uninstall the PIA and generate your own interop.mshtml.dll interop library, it will work correctly on your machine. But it won't work correctly on another machine that doesn't have IE9 installed.

There's a good workaround available however, just don't use the XxxClass classes. Only use the interface types. Which ought to resemble this (tested in a Winforms app):

        var doc = (IHTMLDocument2)webBrowser1.Document.DomDocument;
        var headItems = (IHTMLElementCollection)doc.all.tags("head");
        var scriptObject = (IHTMLScriptElement)doc.createElement("script");
        scriptObject.type = @"text/javascript";
        //scriptObject.text = TTS.TTSWebFactory.GetJavascript();
        var node = (IHTMLDOMNode)headItems.item(null, 0);
        node.appendChild((IHTMLDOMNode)scriptObject);

Where I intentionally used a cast to (IHTMLDOMNode) instead of (IHTMLHeadElement) because that lesser interface was already good enough to access the item element.