Extending the WebBrowser DOM
Any application that uses the WebBrowser control for its User Interface will have to implement some functionality which allows the application to talk with the HTML page and vice versa. Without this, using WebBrowser control will not make much sense, unless you only want to show the users some nice HTML stuff without allowing them to do anything with it.
The HTML itself does not provide any facilities to communicate with a host application. After all HTML itself is nothing but a markup language which specifies what content should appear inside a page, and provides some information about its formatting, positioning, etc. But fortunately the WebBrowser control supports scripting, and you can insert script code inside your pages easily using a SCRIPT HTML tag. The script you insert can be written in various languages; two of them which are most popular, and supported by default are JScript and VBScript. In this article, I use JScript (because it’s my favorite scripting language, and has a syntax similar to C), but you must be able to use VBScript to do what the sample does easily.
So, everything is solved, no? Well, not exactly! The scripting language itself does not do anything useful. What we’ll use is the native COM support in the scripting languages and runtime. Any code written in JScript or VBScript can communicate with COM objects (written in VC++, VB, etc.). The WebBrowser also control exposes its Document Object Model (DOM) using COM objects. For example, the popular
window object inside the browser’s DOM is actually implemented as a COM object supporting the IHTMLWindow2 interface (declared inside mshtml.h). So, in order to provide a communication route from the application to the scripting environment, you need a COM object. You can write the COM object using MFC, or ATL (of course, if you feel like writing a lot of boilerplate code, you can do this in raw C++ as well). This object can even live inside a different module, either in a separate DLL, or even a separate EXE. Of course, putting it inside a different EXE module makes little sense, because after all this COM object is only a means of talking to the scripting environment, and any logic governing it is tightly bound to the application itself.
This COM object, like any COM object, must fulfill a set of contracts. In this case, the object has to be derived from IDispatch. This is the case for any COM object to be accessible from scripting environment, because IDispatch provides general methods for calling a method on an object having its name as a string, and knowing nothing about its binary layout inside the memory.
So, how is the scripting environment going to know about the existence of such an object? As one would guess, the scripting environment calls a method on a predefined interface, and that method will return an IDispatch pointer to the application’s COM object (which should already be created). The method which is responsible for this is IDocHostUIHandler::GetExternal( ). This method returns an IDispatch pointer to the application’s COM object responsible for communication between the scripting environment and the application. If this method succeeds, the scripting environment can access any methods and/or properties of the COM object using their string names. How does the writer of the script code access the COM object? The
external property of the
window object will contain the COM object returned by IDocHostUIHandler::GetExternal( ).
These are the details you need to know before stepping into writing the code. But wait! I talked about the IDocHostUIHandler. What is it? How should one implement it? How should it be passed to the WebBrowser control? You don’t need to worry about these things at all, because I have already provided another article which discusses this interface, and offers a useful class for implementing this interface. You should read this article before proceeding, and also investigate its source code, if you have not done so already.
The sample application which can be downloaded at the bottom of the article uses the classes presented in my Advanced CHtmlView Hosting article to implement the IDocHostUIHandler interface. The sample only overrides one virtual function of the class CDocHostHtmlView. The CExtendingDOMView::OnGetExternal( ) function is overridden to return the IDispatch pointer to the COM object.
The COM object implementation is simple if you have used ATL to write COM objects before. I simply used the ATL Object Wizard to create a Simple Object. To do this, you should select the Insert | New ATL Object menu item. ATL Object Wizard will ask you if you would like to add ATL support to your MFC project. Click Yes to add the necessary support for ATL to your MFC project. Right after you click Yes, you’ll get a message box complaining about an error. Don’t worry, this is the normal behavior! Now you have ATL support added to your project (if you doubt, open stdafx.h and see the lines added to it). Now you have to run the Wizard again, this time you’ll see a list of possible objects you can create. What you need is an ordinary Simple Object (which is the first choice inside the Wizard. Click Next. In the next dialog box, type "ProxyObj" inside the Short Name edit box; you’ll see the rest of the edit boxes update accordingly. Then switch to the Attributes tab, select Single for the Threading Model, and also check the Support ISupportErrorInfo check box. Clicking OK will create the boilerplate code for the object.
Now you have to add methods and properties to your object. The methods and properties you’ll add depend on the application’s needs. I have added some sample methods and properties just for testing purposes. you can check out the sample application to see them. Pay special attention on the RaiseError( ) method. This method intentionally raises an error, to demonstrate how it is handled inside the scripting environment.
Note: Because of a bug, you may get the compiler error "error C2668: ‘InlineIsEqualGUID’ : ambiguous call to overloaded function". The Microsoft Knowledge Base article ID Q243298 describes this problem.
The last thing you should consider is how to pass the COM object you’ve just created to the OnGetExternal( ) function. This should be rather simple. Because the COM object lives inside our own module, you don’t need to bother with the standard way of creating COM objects, which is calling ::CoCreateInstance( ). So, how should you do it? You might think you can simply create an object of the CProxyObj class. But you can’t, because standard IUnknown methods of each interface are not implemented inside CProxyObj. In other words, CProxyObj is an abstract class. What you need to do is to wrap the class inside a CComObject object. To create the actual object, you call the static CComObject::CreateInstance( ) function. Because these are all standard ATL subjects (and I suppose you’re familiar with them) I won’t describe it further. If you’re not familiar with this method of creating COM objects, read up the documentations on the MSDN.
The last decision to make is where should the COM object belong to. You might want to put it inside the document’s class, or the view class. Even you might want to put it inside the main frame class. This really depends on the application. The most generic place for creating such an object (if you don’t want to create a global object) is inside the application’s class; but then again this is the worst solution in my opinion. Anyway, I decided to have the COM object as a member of my view class. Also I added a public method to the application’s class which is responsible for getting an IDispatch pointer to the COM object from the view class. This method is accessed from the OnGetExternal( ) function.
You can build the sample application, and run it. The main page shows the necessary controls for manipulating the COM objects. The JScript source code used inside the page is also shown. See how
window.external is used to access the COM object. Note that if you open the HTML file in IE (the file is mainpage.htm in the src folder) you get errors saying object does not support this property or method. This is because that the
window.external property of IE does not have access to our COM object, and hence cannot resolve method calls using the IDispatch interface.
One last note. The application hosts a COM object, so it should be registered first. You can register it using this command line:
This is the standard way to register COM EXE servers by the way. Also note that if you are going to ship the application, you need to ship the .tlb file generated by the MIDL Compiler, too.
Using these pieces of information, you can build applications which use the WebBrowser control to show their main user interface. By the way, you have probably seen applications which are implemented this way before. Both Outlook Express, and Microsoft Outlook use the same technique I have described to generate their HTML UI, and make it useful. If you like their UI, you can start to create your own HTML UI today. Good luck!
10/31/2003 – Added support for Visual C++ .NET 2003 to the sample project (thanks to Jason DeGeorge).