9

Utilizing several code snippets I've attempted to hook up an ActiveX object with a Javascript event handler. I'm unable to identify why the event handler isn't being called.

Github Repository with project.

Update

By placing the javascript call to SayHello() in an 'onLoad' event, I was able to get the ActiveX event to fire. Now I'm looking toward the C# call, and how to hook it into the ActiveX object utilized by Javascript.

(This may also have relied on enable local scripts from the Advanced options of IE).

Message Continued

The event handler is done in the same form as described for this question.

    <script for="MyObject" event="OnUpdateString(stuff)">
        document.write("<p>" + stuff);
        document.writeln("</p>");
    </script>

Utilizing MSDN documentation I created a WinForms app that contains a WebBrowser control that acts as the ObjectForScripting (not related to the issue). This container makes a call out to the ActiveX event, but is unhandled by the Javascript. I'm including the C# Form code to be complete in the ActiveX interactions and to allow this to be a reference for future users of ActiveX and/or WebBrowser control.

This file is intended to be used with a new Windows Form project where a WebBrowser control was added to the main window.

C# Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Security.Permissions;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using ActiveXObjectSpace;

namespace TestActiveX
{
    [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
    [System.Runtime.InteropServices.ComVisibleAttribute(true)]
    public partial class Form1 : Form
    {
        MyObject myObject = new MyObject();
        public Form1()
        {
            InitializeComponent();
            Text = "ActiveX Test";

            Load += new EventHandler(Form1_Load);
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            webBrowser1.AllowWebBrowserDrop = false;
            webBrowser1.ObjectForScripting = this;
            webBrowser1.Url = new Uri(@"C:\path\to\TestPage.html");

            // Call ActiveX
            myObject.SayHello("C# Launch");
        }

        public string ControlObject()
        {
            return "<p>Control Object Called.</p>";
        }
    }
}

Combining from the help of two other code snippets I created a the ActiveX object. Which, as noted, needs to be registered after being built.

C# ObjectX.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;


/// http://blogs.msdn.com/b/asiatech/archive/2011/12/05/how-to-develop-and-deploy-activex-control-in-c.aspx
/// https://stackoverflow.com/questions/11175145/create-com-activexobject-in-c-use-from-jscript-with-simple-event
///
/// Register with %NET64%\regasm /codebase <full path of dll file>
/// Unregister with %NET64%\regasm /u <full path of dll file>
namespace ActiveXObjectSpace
{

    /// <summary>
    /// Provides the ActiveX event listeners for Javascript.
    /// </summary>
    [Guid("4E250775-61A1-40B1-A57B-C7BBAA25F194"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IActiveXEvents
    {
        [DispId(1)]
        void OnUpdateString(string data);
    }

    /// <summary>
    /// Provides properties accessible from Javascript.
    /// </summary>
    [Guid("AAD0731A-E84A-48D7-B5F8-56FF1B7A61D3"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IActiveX
    {
        [DispId(10)]
        string CustomProperty { get; set; }
    }

    [ProgId("MyObject")]
    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.AutoDual)]
    [Guid("7A5D58C7-1C27-4DFF-8C8F-F5876FF94C64")]
    [ComSourceInterfaces(typeof(IActiveXEvents))]
    public class MyObject : IActiveX
    {

        public delegate void OnContextChangeHandler(string data);
        new public event OnContextChangeHandler OnUpdateString;

        // Dummy Method to use when firing the event
        private void MyActiveX_nMouseClick(string index)
        {

        }

        public MyObject()
        {
            // Bind event
            this.OnUpdateString = new OnContextChangeHandler(this.MyActiveX_nMouseClick);
        }

        [ComVisible(true)]
        public string CustomProperty { get; set; }


        [ComVisible(true)]
        public void SayHello(string who)
        {
            OnUpdateString("Calling Callback: " + who);
        }
    }
}

Last is the html page which is to be loaded by the browser or container. It loads the ActiveX object successfully and contains an event handler for OnUpdateString. It checks that the ActiveX provided function, SayHello, can be called and makes the call.

I'd expect the Javascript and C# call to be written to the document, but no such entries are written.

TestPage.html

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>DemoCSharpActiveX webpage</title>
</head>
<body>
        <script type="text/javascript">
        window.objectLoadFailure = false;
        </script>

        <object id="MyObject" onerror="window.objectLoadFailure = true" classid="clsid:7A5D58C7-1C27-4DFF-8C8F-F5876FF94C64"></object>

        <script for="MyObject" event="OnUpdateString(stuff)">
            document.write("<p>" + stuff);
            document.writeln("</p>");
        </script>


        <script type="text/javascript">
            document.write("<p>Loaded ActiveX Object: " + !window.objectLoadFailure);
            document.writeln("</p>");
            if (typeof window.external.ControlObject !== "undefined") {
                document.write(window.external.ControlObject());
            }


            var obj = document.MyObject;
            if (typeof obj.SayHello !== "undefined") {
                document.writeln("<p>Can Call say hello</p>")
            }
            obj.SayHello("Javascript Load");

        </script>
</body>
</html>

The containing page shows this output

Output

Loaded ActiveX Object: true

Control Object Called.

Can Call say hello

Community
  • 1
  • 1
he_the_great
  • 6,195
  • 2
  • 29
  • 27
  • I can't spot anything else suspicious here, a similar code has certainly [worked for me before](http://stackoverflow.com/a/23733581/1768303). Perhaps, add `type="text/javascript"` to that ` – noseratio May 19 '15 at 22:47
  • @Noseratio Unfortunately none of these suggestions have come to fruition. – he_the_great May 20 '15 at 00:02
  • In this case I suggest you make a complete project (with both the app and the DLL) available to repro this and put a bounty on this question. I'm sure someone will take it from there. – noseratio May 20 '15 at 00:59
  • @Noseratio I like the repo idea, but I'm forced to wait on the bounty. – he_the_great May 20 '15 at 03:19

1 Answers1

3

Updated, as long as you can get the <object> instantiated from HTML (MyObject.object != null), the ultimate problem with your JavaScript event handler is simply that you kill the original HTML document with document.write before you call MyObject.SayHello("Javascript Load"), and replace it with <p>Loaded ActiveX Object: ...</p>. By then, all original JavaScript event handlers are gone.

Thus, the following works fine, the event gets fired and handled (with an alert):

<!DOCTYPE html>
<html>
<head>
    <title>DemoCSharpActiveX webpage</title>
</head>
<body>
    <script type="text/javascript">
        window.objectLoadFailure = false;
    </script>

    <object id="MyObject" onerror="window.objectLoadFailure = true" classid="clsid:7A5D58C7-1C27-4DFF-8C8F-F5876FF94C64"></object>

    <script type="text/javascript" for="MyObject" event="OnUpdateString">
        alert("Hello from event handler");
    </script>

    <script type="text/javascript" for="window" event="onload">
        alert("Hello from window.onload!");
        alert(MyObject.object);
        MyObject.SayHello("Javascript Load");
    </script>
</body>
</html>

To make your original logic work, you can manipulate the DOM directly instead of using document.write. Or, at least call it after OnUpdateString has been fired and handled.


Now that I've seen the full source, I can tell quite a few things going wrong here.
  • You can hit a break point inside SayHello because you create MyObject from C# [MyObject myObject = new MyObject()] and call it from C# [myObject.SayHello("C# Launch")]. Remove that and you'll see it never gets invoked when you call it from JavaScript [obj.SayHello("Javascript Load")].

  • That leads to another issue: the <object> doesn't get create successfully, and even more so, none of your JavaScript scripts even run, because your test HTML file is served from the local file system (via file:// protocol). This is a security restriction. Try changing your script like below to see none of the alerts actually show up:

    <script type="text/javascript" for="window" event="onload">
        alert("Hello from window.onload!");
        alert(MyObject.object) // null! object wasn't created...
        document.write("<p>Loaded ActiveX Object: " + !window.objectLoadFailure);
        document.writeln("</p>");
        if (typeof window.external.ControlObject !== "undefined") {
            document.write(window.external.ControlObject());
        }
    
    
        var obj = document.MyObject;
        if (typeof obj.SayHello !== "undefined") {
            document.writeln("<p>Can Call say hello</p>")
        }
        obj.SayHello("Javascript Load");
    </script>
    
  • There're several ways of fixing it. The easiest one is probably to use "Mark of Web". The hardest one would be to provide a custom implementation of IInternetSecurityManager. I myself would use yet another method - Internet Feature Control - and disable FEATURE_LOCALMACHINE_LOCKDOWN, FEATURE_BLOCK_LMZ_SCRIPT, FEATURE_BLOCK_LMZ_OBJECT keys. You can use following code I adapted from my other related answer:

    // static constructor, runs first
    static Form1()
    {
        SetWebBrowserFeatures();
    }
    
    static void SetWebBrowserFeatures()
    {
        // don't change the registry if running in-proc inside Visual Studio
        if (LicenseManager.UsageMode != LicenseUsageMode.Runtime)
            return;
    
        var appName = System.IO.Path.GetFileName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
    
        var featureControlRegKey = @"HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\";
    
        Registry.SetValue(featureControlRegKey + "FEATURE_BROWSER_EMULATION",
            appName, GetBrowserEmulationMode(), RegistryValueKind.DWord);
    
        // enable the features which are "On" for the full Internet Explorer browser
    
        Registry.SetValue(featureControlRegKey + "FEATURE_ENABLE_CLIPCHILDREN_OPTIMIZATION",
            appName, 1, RegistryValueKind.DWord);
    
        Registry.SetValue(featureControlRegKey + "FEATURE_AJAX_CONNECTIONEVENTS",
            appName, 1, RegistryValueKind.DWord);
    
        Registry.SetValue(featureControlRegKey + "FEATURE_GPU_RENDERING",
            appName, 1, RegistryValueKind.DWord);
    
        Registry.SetValue(featureControlRegKey + "FEATURE_WEBOC_DOCUMENT_ZOOM",
            appName, 1, RegistryValueKind.DWord);
    
        Registry.SetValue(featureControlRegKey + "FEATURE_NINPUT_LEGACYMODE",
            appName, 0, RegistryValueKind.DWord);
    
        Registry.SetValue(featureControlRegKey + "FEATURE_LOCALMACHINE_LOCKDOWN",
            appName, 0, RegistryValueKind.DWord);
    
        Registry.SetValue(featureControlRegKey + "FEATURE_BLOCK_LMZ_SCRIPT",
            appName, 0, RegistryValueKind.DWord);
    
        Registry.SetValue(featureControlRegKey + "FEATURE_BLOCK_LMZ_OBJECT",
            appName, 0, RegistryValueKind.DWord);
    }
    
    static UInt32 GetBrowserEmulationMode()
    {
        int browserVersion = 0;
        using (var ieKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Internet Explorer",
            RegistryKeyPermissionCheck.ReadSubTree,
            System.Security.AccessControl.RegistryRights.QueryValues))
        {
            var version = ieKey.GetValue("svcVersion");
            if (null == version)
            {
                version = ieKey.GetValue("Version");
                if (null == version)
                    throw new ApplicationException("Microsoft Internet Explorer is required!");
            }
            int.TryParse(version.ToString().Split('.')[0], out browserVersion);
        }
    
        if (browserVersion < 7)
        {
            throw new ApplicationException("Unsupported version of Microsoft Internet Explorer!");
        }
    
        UInt32 mode = 11000; // Internet Explorer 11. Webpages containing standards-based !DOCTYPE directives are displayed in IE11 Standards mode. 
    
        switch (browserVersion)
        {
            case 7:
                mode = 7000; // Webpages containing standards-based !DOCTYPE directives are displayed in IE7 Standards mode. 
                break;
            case 8:
                mode = 8000; // Webpages containing standards-based !DOCTYPE directives are displayed in IE8 mode. 
                break;
            case 9:
                mode = 9000; // Internet Explorer 9. Webpages containing standards-based !DOCTYPE directives are displayed in IE9 mode.                    
                break;
            case 10:
                mode = 10000; // Internet Explorer 10.
                break;
        }
    
        return mode;
    }
    
  • Now, your scripts do run, but the <object> still doesn't get created (alert(MyObject.object) shows null). Finally, you'd need to implement IObjectSafety interface on your ActiveX object and site-lock it to only your very own HTML pages. Without proper IObjectSafety the object won't be getting created under default IE security settings. Without site-locking it might become a huge security threat, as any malicious script possibly could create and use your object outside the context of your application.


Updated to address the comment:

I've updated the project with your provided example, note that I had made a change such that there is a C# button and a Javascript button to fire the event. The JS button works, but C# does not fire. I'm looking for a "Hello from: C# button" alert.

In your code, the myObject instance gets created and accessed exclusively from C#:

MyObject myObject = new MyObject();

// ...

private void button1_Click(object sender, EventArgs e)
{
   // Call ActiveX
   myObject.SayHello("C# Button");
}

This instance has nothing to do with the <object id="MyObject" onerror="window.objectLoadFailure = true" classid="clsid:7A5D58C7-1C27-4DFF-8C8F-F5876FF94C64"></object> instance that you create from HTML. They're two separate, unrelated objects. Your event handlers only work for the latter <object> instance. You don't even subscribe to any events on the new MyObject() instance.

If I understand your goal correctly, you need this:

private void button1_Click(object sender, EventArgs e)
{
    // Call ActiveX
    //myObject.SayHello("C# Button");

    this.webBrowser1.Document.InvokeScript("eval",
        new[] { "MyObject.SayHello('C# Button')" });
}

Now, the JavaScript event handler would get invoked and you'd see the "C# Button" alert.

Community
  • 1
  • 1
noseratio
  • 56,401
  • 21
  • 172
  • 421
  • Thank you for attempting an answer, but your premise is all wrong. SayHello was fired for both C# and Javascript. The file protocol is already being used, it just doesn't state the protocol. MyObject.object is not null, it is in fact a 'ActiveXObjectSpace.MyObject'. I did enable the running of local scripts, to no avail. I have not tried IObjectSafety as I'm not interested in restricting what sites have access to run the ActiveX object. The only code missing from the original question was the VS generated Designer.cs file. – he_the_great May 21 '15 at 16:44
  • @he_the_great, what I wrote here applies to the project you've made available, as is. I just downloaded, compiled and ran it. The only correction I made is the path to the test HTML file and did RegAsm. If there are any other prerequisites/steps to make sure the AX object gets created, you should list them. – noseratio May 21 '15 at 17:41
  • @Noserato, I had success with Javascript by moving the SayHello call into an onLoad event. The C# call still does not come through, I've moved it into a button so it can be fire at will after the Javascript finishes loading. – he_the_great May 21 '15 at 18:15
  • I really wanted that to be the answer. The object was being destroyed by the 'write' but only that of the event handler. The C# call still will not fire the event for JS. – he_the_great May 22 '15 at 17:54
  • @he_the_great, the HTML snippet I've posted works perfectly well for me, using your project as a base. I do see the alert from `alert("Hello from event handler")`. Time permitting, I'll upload the complete project. – noseratio May 24 '15 at 17:57
  • I've updated the project with your provided example, note that I had made a change such that there is a C# button and a Javascript button to fire the event. The JS button works, but C# does not fire. I'm looking for a "Hello from: C# button" alert. – he_the_great May 26 '15 at 15:47
  • @he_the_great, I've updated my answer to address your last comment. – noseratio May 26 '15 at 20:15
  • 1
    You have successfully bypassed C# communicating with the ActiveX object used by Javascript. But maybe you are correct in that this is how it is done (I don't have access to client code). – he_the_great May 26 '15 at 21:04
  • @he_the_great, they're many ways to communicate between C# and JavaScript. Check [this](http://stackoverflow.com/a/19002650/1768303) and [this](http://stackoverflow.com/a/21150022/1768303). In your case, you could also obtain a direct reference to `MyObject` from C#: `var myObject = (MyObject)this.webBrowser1.Document.InvokeScript("eval", new[] { "MyObject.object" })`, then call its methods directly. – noseratio May 27 '15 at 00:37