Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Calling JavaScript functions in the Web Browser Control


:P
On this page:

I’ve been working on a sample application that needs to load some generated display content into the Web Browser control and I’ve been wanting to interact with this content. One of the things I want to do is use some transitions like modal overlays and activity displays while pages load – in other words do some Ajax based UI functionality but with data that is provided by the host application.

The process for this goes something like this:

  • Bring up the form
  • Based on parameters provided as input render the form with HTML content
  • Interact with the page (like reload the display as changes are made or stuff data into the dom)

One of the operations that need to occur are re-freshes as new data is loaded so I like to display modal overlay. So in my page I have some script code that uses jQuery and my internal script library with something simple like this:

<div id="modalDialog" style="width: 100px; height: 100px;">
    <img id="loadingImg" src="loading.gif" />
</div>
<
script type="text/javascript"> function showOverlay(hide) { if (!hide) $("#modalDialog").modalDialog(); else $("#modalDialog").modalDialog("hide"); return "Rick" && just to demonstrate ret parameter } </script>

So, is it possible to call this script function in the page? Absolutely. As you probably know global functions are actually defined on the DOM’s window object so you can access scripts through the document.parentWindow  property of the DOM.

Now if you’re using Winforms .NET and the Web Browser control there’s a WebBrowser.Document property, but it doesn’t have a parentWindow property. The Document object .NET exposes is only a wrapper around the ‘real’ DOM COM object that Internet Explorer uses internally. The COM object is what we need to access in order to get at script code.

In order to do this we need to get the COM document object and then use Reflection to walk the hierarchy to do what essentially amounts to:

WebBrowser.Document.DomDocument.parentWindow.showOverlay(false);

Here’s what this looks like inside of a small test form that contains only the Web Browser control and a button:

private void Form1_Load(object sender, EventArgs e)
{
    this.Browser.Navigate(@"C:\temp\html\_preview.htm");
}
private void btnOverlay_Click(object sender, EventArgs e)
{
    // This code is much easier than what I came up with – thanks to Jarle in comments
    // who pointed out the uhm obvious  
    object result = this.Browser.InvokeScript("showOverlay", false);

    MessageBox.Show(result.ToString());

    //// *** Get the COM DOM object (not the .NET Wrapper)
    //object doc = this.Browser.Document.DomDocument;

    //// *** Now you can use Reflection on the COM DOM
    //object win = wwUtils.GetPropertyCom(doc, "parentWindow");

    //// *** Call the JavaScript function and capture the result value
    //object result = wwUtils.CallMethodCom(win, "showOverlay", false);

    //MessageBox.Show(result.ToString());
}

The browser is navigated to testpage on startup and the interactive page is loaded. Then when the button is clicked the the overlay is displayed on top of it. The result looks something like this:

 ScriptedDOM

.NET makes this very easy as it has a Browser.Document.InvokeScript() function that can execute any script on the page with parameters and return a result value easily.

Originally I had completely missed this though and instead used COM Interop as you can see in the commented code. Jarle pointed out the obvious in the comments – bit again by the vast functionality in the framework. I was looking for a way to access the Window property of the document, which is where scripts normally are ‘attached’. Alas, Microsoft used Document.InvokeScript, which is in any case much cleaner than having to create a manual wrapper.

Egg on my face. It isn’t the first time I’ve made a simple solution difficult :-}… But I’m keeping this up here for (my own) reference especially in light of some of the complications in the following section.

You can download the .NET sample code from here.

A few gotchas for FoxPro

When I looked into this again I actually was working on a sample FoxPro piece for a conference presentation which has the same requirements. No matter what I tried though I could not get the function call to work calling the document.parentWindow or document.script object. Whenever I would call the method I’d get a method not found error.

Function Names and Return Object Properties need to be lower case

After a bunch of experimenting and by accident creating a real simple function that had an all lower case name, I figure out that this is a bug/feature of the way FoxPro does COM interop with COM objects. Specifically it deals with casing and the fact that Visual FoxPro forces all COM functions called to lower case. Since JavaScript is case sensitive, the correct case is required in order for a function to be executed. This means effectively that FoxPro can only call lower case Javascript functions. The other issue is that you HAVE TO pass at least one parameter to the called JavaScript function even if that function does not take a parameter. Again this is an oddity in how VFP translates the COM interfaces to to call this dynamic function and passes it one parameter. This shouldn’t be a problem as you can just ignore the parameter in the JavaScript function.

The following can be done from the command window:

oBrowser = CREATEOBJECT("InternetExplorer.Application") obrowser.Visible = .t. oBrowser.Navigate("C:\wwapps\Conf\FoxWebServices\FoxCode\html\_preview.htm") ? oBrowser.Document.parentWindow.showoverlay(.F.) ? oBrowser.Document.Script.showoverlay(.T.)

Note that in order for this to work with the script shown above the function name has to be changed to lowercase showoverlay() in JavaScript!

If you want to return objects from a top level JavaScript function and access the object in FoxPro code all the properties of the object returned also have to be lower case or you get the same unknown name error as above. If you create objects do it like this:

function getselection() {
    var sel = document.selection;
    var range = sel.createRange();
    var parent = range.parentElement();
    
    var parentHtml = null;
    var tagName = null;                
    if (parent) {
        parentHtml = parent.outerHTML;
        tagName = parent.nodeName;
    }
    var selHtml = null;
    if (range)
        selHtml = range.htmlText;                

    var res = {
        selection: sel,
        range: range,
        parent: parent,
        tagname: tagName,
        parenthtml: parentHtml ,
        selectionhtml: selHtml
    }
    return res;
} 

Notice that all the object properties that are returned are lower case and thus accessible in FoxPro. Having properties like parentHtml or tagName on the other hand does not work.

This is really unfortunate behavior of Visual FoxPro, because it prohibits you from calling functions and returning objects from just any JavaScript code loaded into the Web Browser control or InternetExplorer.Application, but at least you can create your own functions and call those.

Works in the Web Browser Control or InternetExplorer.Application

As you can see in the FoxPro snippet this approach also works with the InternetExplorer.Application browser automation as well as in the Web Browser control. As long as you have access to the DOM document this approach should work regardless how the Web browser is hosted. Note that if you run in Internet Explorer though with scripts from the local machine (ie. not a Web Url) you will get a security warning every time you load the page. You have to allow scripts in the yellow warning bar at the top first before they can be accessed. This makes script access in the IE browser a lot less useful. The Web Browser control doesn’t have this limitation as it assumes you have whatever rights your application already has (or doesn’t have).

Nice

Because of the Fox case issue I had actually thought that this feature didn’t work any longer – suspected some sort of security hack – but it turns out the issue is merely a COM translation issue.  This will come in handy especially in .NET as I need to update some code that does some fairly heavy duty HTML editing code. With the ability to run some of that code in script in the browser and taking advantage of jQuery this will be a lot easier!



West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024