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.

Comments (2)

  1. shrib says:

    There is another solution. Your IronPython class could implement IReflect. This would cause the objects to be marshalled as IDispatch to native JScript. Your IReflect implementation can expose all the Python attributes on the object.

    The good part is that you can have a componentized implementation of IReflect that works for all IronPython classes. Once it works for one class, you can drop it onto any other class. The bad news is that its a bit of work to implement IReflect.

    See this link for some information of IReflect:

    http://blogs.msdn.com/shrib/archive/2007/09/04/ireflect-and-idispatch.aspx

  2. The DLR aims to enable dynamic languages like IronPython and IronRuby to access and minipulate objects