Accessing IronPython objects from native Javascript

Today I got a question by e-mail from someone who wanted to implement something like this MSDN article in IronPython. The gist of the article is that there is a property called ObjectForScripting on a WinForms WebBrowser control. If one sets the property to an object, then that object will be available to javascript that executes in the page through window.external. The Form needs to add the ComVisible attribute.

.NET attributes cannot be added to a class from IronPython but fortunately objects created are ComVisible by default. So we can try a direct translation of the code in the article :

 import clr
clr.AddReference("System.Windows.Forms")
from System.Windows.Forms import Application, Form, WebBrowser, MessageBox

class MyForm(Form):
    def __init__(self):
        self.wb = WebBrowser()
        self.Controls.Add(self.wb)
        self.wb.ObjectForScripting = self
        self.wb.DocumentText = """
                               <html>
                                <body>
                                 <button onclick="window.external.Test('hello')">
                                   call client code from script code
                                 </button>
                                </body>
                               </html>
                               """
    def Test(self, msg):
        MessageBox.Show(msg)
    
        
Application.Run(MyForm())

Unfortunately this doesn't work because ObjectForScripting is being set to an instance of the python type MyForm. In the CLR-world this type will be generated dynamically and will be something like IronPython.NewTypes.System.Windows.Form_1$0 and will not contain any of the python methods. You can verify this by doing clr.GetClrType(MyForm).GetMembers() . The easiest solution then is to define an interface in C# with the methods that you want on your type and implement that interface on your type. So define a simple interface like this:

 public interface IWebBrowserInterop
{
    void Test(string message);
}

and in the python code make your class implement that interface:

 from ClassLibrary1 import IWebBrowserInterop

class MyForm(Form, IWebBrowserInterop):
...

Now if you fire up the app and click on the button the MessageBox will popup. You just reached across from unmanaged javascript through COM/.NET to python

Caveat(s):  This is not type-safe. The IronPython runtime caches the types that are generated. So if there is another class in the python code that is "similar" (same base classes/interfaces and not different __slots__-wise) then this type can be reused. So if you are not bound by other factors, you should try to use Managed JScript instead and the hosting APIs to interact with the python objects.