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