Skip to content

activex control

These are the stories that have been posted to the activex control category.

.NET 2.0+ ActiveX Controls Part 2 – Accessing Internet Explorer via IWebBrowser


Published to Rick Minerich's Development Wonderland by Richard Minerich July 14, 2009 16:04

After spending much time evaluating different approaches, I’ve found a way to easily and reliably grab the URL of my hosting page from inside of a C# based ActiveX Control.  This process involves using dynamic COM invocation to obtain the IWebBrower interface via my ActiveX control’s implicit IOleObject interface.

 

Introduction

IWebBrowser is useful for many things beyond just getting the currently displayed URL.  In fact, it can control almost every aspect of Internet Explorer.  It can be used to completely control browser navigation, redirecting the browser to different pages, refreshing or even going forward and back.

Also, it would be possible to go about this via a statically generated interface to shdocvw.dll.  However, using a static interface has the distinct disadvantage of failing quite explosively if the dll is not available.  Dynamic invocation is much safer.  If something goes wrong we will simply get back null values instead of what we expected.

If you don’t have much ActiveX experience, there are three important pieces of information to know about before starting:

  1. Everything in COM is referenced by GUID.
  2. All COM access is done through querying existing COM objects.
  3. With a little syntactic sugar, C# takes care of much of the pain involved.

 

Defining our COM Interfaces

First we need to define the COM reference Guids for our top level browser service and the Internet Explorer application.

  1: public static class IEGuids
  2: {
  3:     public static Guid GetTopLevelBrowserGuid() 
  4:     {
  5:         return new Guid("4C96BE40-915C-11CF-99D3-00AA004AE837"); 
  6:     }
  7: 
  8:     public static Guid GetWebBrowserAppGuid() 
  9:     { 
 10:         return new Guid("0002DF05-0000-0000-C000-000000000046"); 
 11:     }       
 12: }

Secondly, we need to define the IServiceProvider COM interface.

  1: [ComImport]
  2: [Guid("6d5140c1-7436-11ce-8034-00aa006009fa")]
  3: [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  4: interface IServiceProvider
  5: {
  6:     void QueryService(
  7:         ref Guid guidService, 
  8:         ref Guid riid,
  9:         [MarshalAs(UnmanagedType.Interface)] 
 10:         out object ppvObject);
 11: }

Third, we need to define the IWebBrowser COM interface. 

  1: [ComImport, 
  2:  TypeLibType((short)0x1050), 
  3:  Guid("EAB22AC1-30C1-11CF-A7EB-0000C05BAE0B")]
  4: public interface IWebBrowser
  5: {
  6:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(100)]
  7:   void GoBack();
  8:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x65)]
  9:   void GoForward();
 10:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x66)]
 11:   void GoHome();
 12:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x67)]
 13:   void GoSearch();
 14:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x68)]
 15:   void Navigate([In, MarshalAs(UnmanagedType.BStr)] string URL, [In, Optional, MarshalAs(UnmanagedType.Struct)] ref object Flags, [In, Optional, MarshalAs(UnmanagedType.Struct)] ref object TargetFrameName, [In, Optional, MarshalAs(UnmanagedType.Struct)] ref object PostData, [In, Optional, MarshalAs(UnmanagedType.Struct)] ref object Headers);
 16:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(-550)]
 17:   void Refresh();
 18:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x69)]
 19:   void Refresh2([In, Optional, MarshalAs(UnmanagedType.Struct)] ref object Level);
 20:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x6a)]
 21:   void Stop();
 22:   [DispId(200)]
 23:   object Application { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(200)] get; }
 24:   [DispId(0xc9)]
 25:   object Parent { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xc9)] get; }
 26:   [DispId(0xca)]
 27:   object Container { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xca)] get; }
 28:   [DispId(0xcb)]
 29:   object Document { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xcb)] get; }
 30:   [DispId(0xcc)]
 31:   bool TopLevelContainer { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xcc)] get; }
 32:   [DispId(0xcd)]
 33:   string Type { [return: MarshalAs(UnmanagedType.BStr)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xcd)] get; }
 34:   [DispId(0xce)]
 35:   int Left { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xce)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xce)] set; }
 36:   [DispId(0xcf)]
 37:   int Top { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xcf)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xcf)] set; }
 38:   [DispId(0xd0)]
 39:   int Width { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xd0)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xd0)] set; }
 40:   [DispId(0xd1)]
 41:   int Height { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xd1)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xd1)] set; }
 42:   [DispId(210)]
 43:   string LocationName { [return: MarshalAs(UnmanagedType.BStr)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(210)] get; }
 44:   [DispId(0xd3)]
 45:   string LocationURL { [return: MarshalAs(UnmanagedType.BStr)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xd3)] get; }
 46:   [DispId(0xd4)]
 47:   bool Busy { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xd4)] get; }
 48: }

It’s quite a large and so I won’t go into the details of each member.  You can find out more by reading the IWebBrowser2 MSDN article.  It’s an extension of the IWebBrowser interface which means the overlapping members will have the same effect.

 

Using our COM Interface

Now that we have all of the necessary infrastructure in place, we must build a set of COM queries in order to obtain our IWebBrowser instance.  The first step is getting our ActiveX control’s IOleObject interface.

  1: Type type = control.GetType();
  2: Type iOleObjectType = type.GetInterface("IOleObject", true);

We then use the iOleObjectType interface to retrieve the client site service provider from our ActiveX control.

  1: if (iOleObjectType != null)
  2: {
  3:   oleClientSiteObj = iOleObjectType.InvokeMember(
  4:     "GetClientSite",
  5:     BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public,
  6:     null, control, null);
  7:   serviceProvider = oleClientSiteObj as IServiceProvider;
  8: }

By querying our COM Object’s client side service provider, we can obtain shdocvw.dll’s COM interface via it’s GUID and the GUID of the interface.

  1: if (serviceProvider != null)
  2: {
  3: 	Guid topLevelBrowserGuid = _topLevelBrowserGuid;
  4: 	Guid iServiceProviderGuid = typeof(IServiceProvider).GUID;
  5: 	serviceProvider.QueryService(
  6: 		ref topLevelBrowserGuid,
  7: 		ref iServiceProviderGuid,
  8: 		out topServiceProviderObj);
  9: 	topServiceProvider = topServiceProviderObj as IServiceProvider;
 10: }

Finally, in a similar way, we can now obtain the IWebBrowser interface from Internet Explorer’s COM interface via their GUIDs.

  1: if (topServiceProvider != null)
  2: {
  3: 	Guid webBrowserAppGuid = _webBrowserAppGuid;
  4: 	Guid iWebBrowserGuid = typeof(IWebBrowser).GUID;
  5: 	topServiceProvider.QueryService(
  6: 		ref webBrowserAppGuid,
  7: 		ref iWebBrowserGuid,
  8: 		out webServiceProviderObj);
  9: 	webBrowser = webServiceProviderObj as IWebBrowser;
 10: }

Now, we have an instance the IWebBrowser interface.   For my purposes, I need only the URL currently displayed in the browser window.

  1: if (webBrowser != null)
  2: {
  3: 	url = webBrowser.LocationURL;
  4: }

 

Conclusion

Using the IWebBrowser allows you to access and control Internet Explorer in a way which is otherwise unavailable for .NET ActiveX Controls.  Thanks to .NET’s excellent COM interop capabilities it’s possible to do it easily in a safe and reliable way.  Also, this technique is known to work for IE 6 all the way through the newest versions of Internet Explorer.  For future reference, at the time of this article the newest is IE8.0.6001.18702.

 

References and Additional Information

.NET 2.0 ActiveX Control Gotchas (Safe for Scripting and Hooking into Events)


Published to Rick Minerich's Development Wonderland by Richard Minerich June 03, 2009 17:41

I’ve recently been building an ActiveX Control in .NET 2.0 and thought I would share some of the problems I’ve run into, as well as their solutions.  I hope that in reading this you can avoid a few of the timesinks I fell into. 

 

Safe for Scripting

image

By default, ActiveX controls are not marked as safe for scripting.  This means that Internet Explorer will refuse to run a control given it’s default settings, even for sites in it’s Trusted security zone.  Thankfully, this is an easy problem to correct.

 

The Wrong Way

image

It’s possible to hide this issue on the client machine by setting “Initialize and script ActiveX controls not marked safe for scripting.” to true or prompt.  This, of course, is not an acceptable solution as it will require all clients to do the same and in so doing potentially open them to malicious controls.

 

The Correct Solution

There are two ways to mark an ActiveX control as safe for scripting.  The first, and easiest in the context of .NET, is to implement the IObjectSafety interface.  The only caveat to this method is that it requires that you can modify the ActiveX control’s source code. 

The second, more complex option, is to use COM Component Categories Manager.  While not requiring source changes and recompilation, this method requires a rather large amount of registry editing.  As I did not take this approach, I won’t delve into it further.  Additional .NET implementation information is available in this CodeProject article.  

 

IObjectSafety

Implementation requires first importing the IObjectSafety interface.  This is a simply a matter of declaring a interface with the ComImport attribute.

While in most cases it is extremely important to ensure the Guid tags on your interface declarations are unique, in this case it equally important not to change it.  This is because the GUID attribute here is that of the IObjectSafety interface.  To put it plainly, changing the Guid in the following example will cause it to not work.

  1: [Flags]
  2: public enum IObjectSafetyOpts : int //DWORD
  3: {
  4:     // Object is safe for untrusted callers.
  5:     INTERFACESAFE_FOR_UNTRUSTED_CALLER  = 0x00000001,
  6:     // Object is safe for untrusted data.
  7:     INTERFACESAFE_FOR_UNTRUSTED_DATA    = 0x00000002,
  8:     // Object uses IDispatchEx.
  9:     INTERFACE_USES_DISPEX               = 0x00000004,
 10:     // Object uses IInternetHostSecurityManager.
 11:     INTERFACE_USES_SECURITY_MANAGER     = 0x00000008
 12: }
 13: 
 14: public enum IObjectSafetyRetVals : uint //HRESULT
 15: {
 16:     //The object is safe for loading.
 17:     S_OK            = 0x0,
 18:     //The riid parameter specifies an interface that is unknown to the object.
 19:     E_NOINTERFACE   = 0x80000004
 20: }
 21: 
 22: [ComImport()]
 23: //This GUID is that of IObjectSafety. Do not replace!
 24: [Guid("CB5BDC81-93C1-11CF-8F20-00805F2CD064")] 
 25: [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
 26: public interface IObjectSafety
 27: {
 28:     [PreserveSig()]
 29:     IObjectSafetyRetVals GetInterfaceSafetyOptions(ref Guid riid, out IObjectSafetyOpts supportedOpts, out IObjectSafetyOpts enabledOpts);
 30:     [PreserveSig()]
 31:     IObjectSafetyRetVals SetInterfaceSafetyOptions(ref Guid riid, IObjectSafetyOpts optsMask, IObjectSafetyOpts enabledOpts);
 32: }
 33: 

You then need only implement this interface in your ActiveX control as follows.

  1: ...
  2: public partial class ExampleControl : IObjectSafety
  3: {
  4:     public IObjectSafetyRetVals GetInterfaceSafetyOptions(ref Guid riid, out IObjectSafetyOpts supportedOpts, out IObjectSafetyOpts enabledOpts)
  5:     {
  6:         supportedOpts = IObjectSafetyOpts.INTERFACESAFE_FOR_UNTRUSTED_CALLER | IObjectSafetyOpts.INTERFACESAFE_FOR_UNTRUSTED_DATA;
  7:         enabledOpts = IObjectSafetyOpts.INTERFACESAFE_FOR_UNTRUSTED_CALLER | IObjectSafetyOpts.INTERFACESAFE_FOR_UNTRUSTED_DATA;
  8:         return IObjectSafetyRetVals.S_OK;
  9:     }
 10: 
 11:     public IObjectSafetyRetVals SetInterfaceSafetyOptions(ref Guid riid, IObjectSafetyOpts optsMask, IObjectSafetyOpts enabledOpts)
 12:     {
 13:         return IObjectSafetyRetVals.S_OK;
 14:     }
 15:     ...
 16: }
 17: 

With the IObjectSafety interface implemented to return INTERFACESAFE_FOR_UNTRUSTED_CALLER and INTERFACESAFE_FOR_UNTRUSTED_DATA, your object is considered scripting safe for use by Internet Explorer.  Your control should no longer require any non-default ActiveX related settings to run.

INTERFACE_USES_DISPEX and INTERFACE_USES_SECURITY_MANAGER are mainly used for scripting engines and can be safely ignored.

 

 

Hooking into Events

Working on my current project, I spent a not insignificant amount of time working to make events fire correctly.  I started with a simple implementation, similar to what is discussed in this article.

  1: <object id="ActiveXExample" name="ActiveXExample" 
  2:   classid="clsid:21192EDE-868C-4b94-9D20-B822C42EA9D2" 
  3:   codebase="ActiveX.cab#version=1,0,0,0" VIEWASTEXT>
  4: </object>
  1: [Guid("C07F993D-242D-4c1e-AF1B-B77CAE5FD088")]
  2: [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
  3: public interface IExposedComEvents
  4: {
  5:     [DispIdAttribute(0x60020001)]
  6:     void ExampleEvent(string text);
  7: }
  8: 
  9: [Guid("21192EDE-868C-4b94-9D20-B822C42EA9D2")]
 10: [ClassInterface(ClassInterfaceType.None),
 11:  ComSourceInterfaces(typeof(IExposedComEvents))]
 12: [ComVisible(true)]
 13: public partial class ActiveXExample
 14: {
 15:     public event ExampleEventHandler ExampleEvent;
 16:     ...
 17: }
 18: 
  1: <script language="javascript">
  2: function ActiveXExample::ExampleEvent(text)
  3:     try {
  4:         elem = document.getElementById("status");
  5:         elem.innerHTML = text;
  6:     } catch(exception) {
  7:         alert("Exception Thrown in Event: " + exception);
  8:     }
  9: </script>

However, I found that my javascript events were not being registered.  After some fiddling, I discovered that I was able to capture the event by instead using the “for-object event” script tag.

  1: <script for="ActiveXExample" event="ExampleEvent(text)">
  2: try {
  3:     elem = document.getElementById("status");
  4:     elem.innerHTML = text;
  5: } catch(exception) {
  6:     alert("Exception Thrown in Event: " + exception);
  7: }
  8: </script>
  9: 

I’m still not quite sure why the Object::Event syntax didn’t work, as it is shown often in ActiveX sample code.  Still, if you find you are having issues getting events to fire correctly, this alternate syntax may be worth trying.

 

Additional Resources

MSDN: Safe Initialization and Scripting for ActiveX Controls

Eric Lippert's Fantastic Eight Part Series: Script and IE Security 
     One, Two, Three, Four, Five, Six, Seven and Eight

.NET 2.0+ ActiveX Controls Part 2 – Controlling Internet Explorer via IWebBrowser


Published to Rick Minerich's Development Wonderland by Richard Minerich July 14, 2009 16:11

After spending much time evaluating different approaches, I’ve found a way to easily and reliably grab the URL of my hosting page from inside of a C# based ActiveX Control.  This process involves using dynamic COM invocation to obtain the IWebBrower interface via my ActiveX control’s implicit IOleObject interface.

 

Introduction

IWebBrowser is useful for many things beyond just getting the currently displayed URL.  In fact, it can control almost every aspect of Internet Explorer.  It can be used to completely control browser navigation, redirecting the browser to different pages, refreshing or even going forward and back.

Also, it would be possible to go about this via a statically generated interface to shdocvw.dll.  However, using a static interface has the distinct disadvantage of failing quite explosively if the dll is not available.  Dynamic invocation is much safer.  If something goes wrong we will simply get back null values instead of what we expected.

If you don’t have much ActiveX experience, there are three important pieces of information to know about before starting:

  1. Everything in COM is referenced by GUID.
  2. All COM access is done through querying existing COM objects.
  3. With a little syntactic sugar, C# takes care of much of the pain involved.

 

Defining our COM Interfaces

First we need to define the COM reference Guids for our top level browser service and the Internet Explorer application.

  1: private static readonly Guid _topLevelBrowserGuid = new Guid("4C96BE40-915C-11CF-99D3-00AA004AE837");
  2: private static readonly Guid _webBrowserAppGuid = new Guid("0002DF05-0000-0000-C000-000000000046"); 

Secondly, we need to define the IServiceProvider COM interface.

  1: [ComImport,
  2:  Guid("6d5140c1-7436-11ce-8034-00aa006009fa"),
  3:  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  4: public interface IServiceProvider
  5: {
  6:   void QueryService(
  7:     ref Guid guidService, 
  8:     ref Guid riid,
  9:     [MarshalAs(UnmanagedType.Interface)] out object ppvObject);
 10: }

Third, we need to define the IWebBrowser COM interface. 

  1: [ComImport, 
  2:  TypeLibType((short)0x1050), 
  3:  Guid("EAB22AC1-30C1-11CF-A7EB-0000C05BAE0B")]
  4: public interface IWebBrowser
  5: {
  6:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(100)]
  7:   void GoBack();
  8:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x65)]
  9:   void GoForward();
 10:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x66)]
 11:   void GoHome();
 12:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x67)]
 13:   void GoSearch();
 14:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x68)]
 15:   void Navigate([In, MarshalAs(UnmanagedType.BStr)] string URL, [In, Optional, MarshalAs(UnmanagedType.Struct)] ref object Flags, [In, Optional, MarshalAs(UnmanagedType.Struct)] ref object TargetFrameName, [In, Optional, MarshalAs(UnmanagedType.Struct)] ref object PostData, [In, Optional, MarshalAs(UnmanagedType.Struct)] ref object Headers);
 16:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(-550)]
 17:   void Refresh();
 18:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x69)]
 19:   void Refresh2([In, Optional, MarshalAs(UnmanagedType.Struct)] ref object Level);
 20:   [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0x6a)]
 21:   void Stop();
 22:   [DispId(200)]
 23:   object Application { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(200)] get; }
 24:   [DispId(0xc9)]
 25:   object Parent { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xc9)] get; }
 26:   [DispId(0xca)]
 27:   object Container { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xca)] get; }
 28:   [DispId(0xcb)]
 29:   object Document { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xcb)] get; }
 30:   [DispId(0xcc)]
 31:   bool TopLevelContainer { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xcc)] get; }
 32:   [DispId(0xcd)]
 33:   string Type { [return: MarshalAs(UnmanagedType.BStr)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xcd)] get; }
 34:   [DispId(0xce)]
 35:   int Left { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xce)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xce)] set; }
 36:   [DispId(0xcf)]
 37:   int Top { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xcf)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xcf)] set; }
 38:   [DispId(0xd0)]
 39:   int Width { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xd0)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xd0)] set; }
 40:   [DispId(0xd1)]
 41:   int Height { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xd1)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xd1)] set; }
 42:   [DispId(210)]
 43:   string LocationName { [return: MarshalAs(UnmanagedType.BStr)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(210)] get; }
 44:   [DispId(0xd3)]
 45:   string LocationURL { [return: MarshalAs(UnmanagedType.BStr)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xd3)] get; }
 46:   [DispId(0xd4)]
 47:   bool Busy { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0xd4)] get; }
 48: }

It’s quite a large and so I won’t go into the details of each member.  You can find out more by reading the IWebBrowser2 MSDN article.  It’s an extension of the IWebBrowser interface which means the overlapping members will have the same effect.

 

Using our COM Interfaces

Now that we have all of the necessary infrastructure in place, we must build a set of COM queries in order to obtain our IWebBrowser instance.  The first step is getting our ActiveX control’s IOleObject interface.

  1: Type type = control.GetType();
  2: Type iOleObjectType = type.GetInterface("IOleObject", true);

We then use the iOleObjectType interface to retrieve the client site service provider from our ActiveX control.

  1: if (iOleObjectType != null)
  2: {
  3:   oleClientSiteObj = iOleObjectType.InvokeMember(
  4:     "GetClientSite",
  5:     BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public,
  6:     null, control, null);
  7:   serviceProvider = oleClientSiteObj as IServiceProvider;
  8: }

By querying our COM Object’s client side service provider, we can obtain shdocvw.dll’s COM interface via it’s GUID and the GUID of the interface.

  1: if (serviceProvider != null)
  2: {
  3:   Guid topLevelBrowserGuid = _topLevelBrowserGuid;
  4:   Guid iServiceProviderGuid = typeof(IServiceProvider).GUID;
  5:   serviceProvider.QueryService(
  6:     ref topLevelBrowserGuid,
  7:     ref iServiceProviderGuid,
  8:     out topServiceProviderObj);
  9:   topServiceProvider = topServiceProviderObj as IServiceProvider;
 10: }

Finally, in a similar way, we can now obtain the IWebBrowser interface from Internet Explorer’s COM interface via their GUIDs.

  1: if (topServiceProvider != null)
  2: {
  3:   Guid webBrowserAppGuid = _webBrowserAppGuid;
  4:   Guid iWebBrowserGuid = typeof(IWebBrowser).GUID;
  5:   topServiceProvider.QueryService(
  6:     ref webBrowserAppGuid,
  7:     ref iWebBrowserGuid,
  8:     out webServiceProviderObj);
  9:   webBrowser = webServiceProviderObj as IWebBrowser;
 10: }

Now, we have an instance the IWebBrowser interface.   For my purposes, I need only the URL currently displayed in the browser window.

  1: if (webBrowser != null)
  2: {
  3:   url = webBrowser.LocationURL;
  4: }

 

Conclusion

Using the IWebBrowser allows you to access and control Internet Explorer in a way which is otherwise unavailable for .NET ActiveX Controls.  Thanks to .NET’s excellent COM interop capabilities it’s possible to do it easily in a safe and reliable way.  Also, this technique is known to work for IE 6 all the way through the newest versions of Internet Explorer.  For future reference, at the time of this article the newest is IE8.0.6001.18702.

 

References and Additional Information

VerCache Madness with .NET ActiveX Controls


Published to Rick Minerich's Development Wonderland by Richard Minerich July 13, 2010 15:37

About a year ago I was working on building our DotTwain ActiveX control and wrote two articles on some useful tips that I discovered.  Since that time, I’ve seen a problem on a few customer computers where, after upgrading once, the control would never run.  Instead, it would just try to reinstall after every page load.

After much frustration, it turned out that the problem is related to a little-known Internet Explorer registry entry called VerCache. 

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Ext\Settings\{GUID}] "VerCache"

I figured this out after noticing that every machine exhibiting this problem had the same value for VerCache.  Once the VerCache was set to this value, uninstalling did nothing.  The repeated-reinstallation behavior happened with any new version installed which shared the control GUID.

 

 

What’s a VerCache?

Strangely enough, searching for “VerCache” ActiveX gives you little more than an IE Team blog post and one MSDN forums entry.

At first, I was very excited when I found this IE team blog post.  It appeared that I had found my solution.

Here in the above mentioned screenshots they both are having same file date time stamps and that Causes the VerCache registry key to not get updated.

To resolve this , ensure that at least one of these parameters - “Created” date time stamp, “Modified” date time stamp or the file size, on the updated control is different from the old version of the control and you should be GTG!

Unfortunately, this information seems to be incorrect, or at least overly vague.  I observed the same bad VerCache value on different machines with different version of the control.  Each of these had different “created” and “modified” time stamps.  In fact, the CAB, the installer in the CAB, and the control assembly itself each had different “created” and “modified” time stamps for every single version.  I even tried updating the INF and setup launcher timestamps to no avail.

The only thing which did not vary consistently was file size as we rebuild and repackage our control for each DotImage version we release.

 

 

Some of the things I tried or considered.

Early on, I discovered simply deleting the registry key after uninstalling fixed the issue temporarily and was under the impression that it was a single bad version of the control causing this.  So, I wrote a small cleaner utility to fix the issue.  However, as time wore on it was apparent that this issue occurred in a somewhat random fashion with all versions of the control.

Another simple fix I thought about was updating the control GUID with each version.  However, this would require updating both our documentation and demos with every build.  Additionally, our customers would need to update their javascript with every upgrade.  This was unacceptable.

The next thing I tried was to have the installer launch a custom console program on install which cleaned the registry value.  This worked great in XP.  However, in Windows 7 the console program, even as admin, did not have access to the registry values I wanted to delete.

 

 

Finally, a solution.

In the end the only solution I found worked consistently and met our needs was to put custom installer actions into the control itself.  When both installing and uninstalling the installer calls the control’s custom actions which removes any existing VerCache value for the control.

 

First, I snagged a class I had made previously for cleaning guid-based registry entries:

Code Snippet
  1. public delegate bool RegActionBreakOnTrue(RegistryKey basekey, string key);
  2.  
  3. public class EventArgs<T> : EventArgs { private T eventData; public EventArgs(T eventData) { this.eventData = eventData; } public T EventData { get { return eventData; } } }
  4.  
  5. public class RegGuidCleaner
  6. {
  7.     string _guid;
  8.     public RegGuidCleaner(string guid)
  9.     {
  10.         _guid = guid;
  11.         Locations = new string[] {};
  12.     }
  13.  
  14.     public string[] Locations { get; set; }
  15.  
  16.     public RegistryKey GetPathBase(string path)
  17.     {
  18.         RegistryKey baseKey;
  19.         if (path.StartsWith(@"HKEY_CURRENT_USER\"))
  20.             baseKey = Registry.CurrentUser;
  21.         else if (path.StartsWith(@"HKEY_LOCAL_MACHINE\"))
  22.             baseKey = Registry.LocalMachine;
  23.         else
  24.             throw new ApplicationException("Unexpected location type: " + path);
  25.         return baseKey;
  26.     }
  27.  
  28.     private string GetBaselessPath(string location)
  29.     {
  30.         string truncatedLoc = location.Substring(location.IndexOf('\\') + 1);
  31.         return truncatedLoc;
  32.     }
  33.  
  34.     public void PerformRegActionForEach(RegActionBreakOnTrue action)
  35.     {
  36.         foreach (var location in Locations)
  37.         {
  38.             string fullPath = location + _guid;
  39.             RegistryKey baseKey = GetPathBase(fullPath);
  40.             string baselessPath = GetBaselessPath(fullPath);
  41.  
  42.             if (action(baseKey, baselessPath))
  43.                 break;
  44.         }
  45.     }
  46.  
  47.     public bool EnteriesExist
  48.     {
  49.         get
  50.         {
  51.             bool doesKeyExist = false;
  52.             PerformRegActionForEach((baseKey, subkey) =>
  53.             {
  54.                 var key = baseKey.OpenSubKey(subkey);
  55.                 if (key != null)
  56.                 {
  57.                     doesKeyExist = true;
  58.                     return true;
  59.                 }
  60.                 else
  61.                     return false;
  62.             });
  63.             return doesKeyExist;
  64.         }
  65.     }
  66.  
  67.     public void DeleteEntries()
  68.     {
  69.         PerformRegActionForEach((baseKey, subkey) =>
  70.         {
  71.             try
  72.             {
  73.                 Messages(this, new EventArgs<String>("Deleting Registry Key: " + baseKey.ToString() + @"\" + subkey));
  74.                 baseKey.DeleteSubKeyTree(subkey);
  75.             }
  76.             catch (Exception ex)
  77.             {
  78.                 Messages(this, new EventArgs<String>(ex.ToString()));
  79.             }
  80.  
  81.             return false;
  82.         });
  83.     }
  84.  
  85.     public event EventHandler<EventArgs<String>> Messages = delegate { };
  86. }

Please forgive the catch (Exception ex).  So far it has been unnecessary to handle failure cases for anything other than logging.

 

Next, I added a custom installer class to remove the entries:

Code Snippet
  1. [RunInstaller(true)]
  2. public partial class RegistryCleanerInstallerClass : Installer
  3. {
  4.     public RegistryCleanerInstallerClass()
  5.     {
  6.         InitializeComponent();
  7.     }
  8.  
  9.     private void CleanVerCache()
  10.     {
  11.         try
  12.         {
  13.             RegGuidCleaner cleaner = new RegGuidCleaner("{" + typeof(AcquisitionControl).GUID + "}");
  14.             cleaner.Locations = new string[] {
  15.                 @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Ext\Settings\",
  16.              };
  17.  
  18.             cleaner.DeleteEntries();
  19.         }
  20.         catch { }
  21.     }
  22.  
  23.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  24.     public override void Install(IDictionary stateSaver)
  25.     {
  26.         base.Install(stateSaver);
  27.         CleanVerCache();
  28.     }
  29.  
  30.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  31.     public override void Commit(IDictionary savedState)
  32.     {
  33.         base.Commit(savedState);
  34.     }
  35.  
  36.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  37.     public override void Rollback(IDictionary savedState)
  38.     {
  39.         base.Rollback(savedState);
  40.         CleanVerCache();
  41.     }
  42.  
  43.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  44.     public override void Uninstall(IDictionary savedState)
  45.     {
  46.         base.Uninstall(savedState);
  47.         CleanVerCache();
  48.     }
  49. }

 

I also decided to move the com registration into a custom action as well.  The installer-based registration had been intermittently not working which caused us to hand test every release version of the control before sending it out.

Code Snippet
  1. [RunInstaller(true)]
  2. public partial class ComRegisterInstallerClass : Installer
  3. {
  4.     public ComRegisterInstallerClass()
  5.     {
  6.         InitializeComponent();
  7.     }
  8.  
  9.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  10.     public override void Install(IDictionary stateSaver)
  11.     {
  12.         base.Install(stateSaver);
  13.  
  14.         try
  15.         {
  16.             RegistrationServices regSrv = new System.Runtime.InteropServices.RegistrationServices();
  17.  
  18.             if (!regSrv.RegisterAssembly(
  19.                 this.GetType().Assembly, AssemblyRegistrationFlags.SetCodeBase))
  20.             {
  21.                 throw new InstallException("Failed to register componenet for COM interop");
  22.             }
  23.         }
  24.         catch
  25.         {
  26.         }
  27.     }
  28.  
  29.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  30.     public override void Commit(IDictionary savedState)
  31.     {
  32.         base.Commit(savedState);
  33.     }
  34.  
  35.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  36.     public override void Rollback(IDictionary savedState)
  37.     {
  38.         base.Rollback(savedState);
  39.  
  40.         try
  41.         {
  42.             RegistrationServices regSrv = new System.Runtime.InteropServices.RegistrationServices();
  43.  
  44.             if (!regSrv.UnregisterAssembly(this.GetType().Assembly))
  45.             {
  46.                 throw new InstallException("Failed to unregister componenet for COM interop");
  47.             }
  48.         }
  49.         catch
  50.         {
  51.         }
  52.     }
  53.  
  54.     [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  55.     public override void Uninstall(IDictionary savedState)
  56.     {
  57.         base.Uninstall(savedState);
  58.  
  59.         try
  60.         {
  61.             RegistrationServices regSrv = new System.Runtime.InteropServices.RegistrationServices();
  62.  
  63.             if (!regSrv.UnregisterAssembly(this.GetType().Assembly))
  64.             {
  65.                 throw new InstallException("Failed to unregister componenet for COM interop");
  66.             }
  67.         }
  68.         catch
  69.         {
  70.         }
  71.     }
  72. }

 

Finally, I added the the custom action calls to the installer.

customAction1

customAction2

 

This still won’t allow an existing broken control to upgrade.  However, after manually uninstalling the old control, the new one will install and work fine.  At least that’s what I’ve found in IE 8.0.7600.16385 Update 0 on Windows 7 Enterprise 64-bit.  ActiveX tends to be a bit of a moving target these days.

Now, let’s hope I never need speak of this again.