Introduction
Embedding a WebBrowser
control in a Windows form has become trivially simple. However, any app that does this will probably want to manipulate the content displayed by that WebBrowser
. Perhaps, the most basic manipulation is hooking into the DocumentComplete
event; before you do anything with the page, you'll need to know that the page - the DOM - is ready for you to begin hacking on it.
Unfortunately, DocumentComplete
won't fire on all systems! Search the web for "DocumentComplete
not firing" - it's a surprisingly widespread problem. The core reason is that the Microsoft.Mshtml assembly isn't installed on all machines. Compounding the problem is the fact that Visual Studio does install the assembly (so do a few Office apps), so you'll never see the bug on a dev system. In addition to not receiving DocumentComplete
, without MSHTML, you'll not be able to do anything useful with the DOM and will get exceptions, should you try.
This article is my solution to the problem. I created a class that traps the absence of Microsoft.mshtml and installs it as needed, all without bothering the user who likely wouldn't know what MSHTML is, let alone know about gacutil.
Background
This article requires familiarity with the WebBrowser
control and the use of .NET assemblies.
It'll be very useful to have/find a machine that doesn't have MSHTML installed.
Using the Code
The attached demo project outlines the technique. The demo implements a simple form with an embedded WebBrowser
; upon loading, the browser is navigated to codeproject.com; when the DocumentComplete
fires, I show a MessageBox
.
The important stuff is in MshtmlAssemblyHelper.cs, but first let me describe some items that will allow you to hook into that functionality.
Directing the CLR to Use the Correct Version of MSHTML
First, you need to tell your app that you require MSHTML version 7.0.3300.0 or higher, which also covers the "Mshtml not installed" case. This is done via an entry in your app.config file:
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.mshtml"
publicKeyToken="b03f5f7f11d50a3a"
culture="neutral" />
<bindingRedirect oldVersion="1.0.0.0-7.0.3299.9"
newVersion="7.0.3300.9" />
</dependentAssembly>
</assemblyBinding>
</runtime>
The above <runtime>
block will instruct the CLR to use MSHTML v7.0.3300.0, and if it can't find that version (or higher, or any MSHTML assembly), the CLR should ask my app to locate the assembly DLL. This last bit - Assembly Resolution - is a central trick to the demo, and will be discussed more below. The most relevant bits of the above XML are the oldVersion
and newVersion
. I set the oldVersion
to be anything lower than 7.0.3300.0 (essentially any version). The newVersion
is my target version: the version of my known-good MSHTML assembly that was installed with my copy of VS2008.
An additional item to note is that I do not have a reference to MSHTML in my project. With the demo, the CLR's desire to load MSHTML is purely driven by the entry in app.config. Of course, any app that wishes to touch the DOM in the WebBrowser
would need to add a reference to MSHTML, but I specifically omitted that in the demo to highlight the role of app.config.
ResolveEventHandler
System.ResolveEventHandler
is a delegate that the CLR will call when it needs your help in finding an assembly. Because of the entries in app.config, if the CLR fails to find a copy of MSHTML version >= 7.0.3300.0 installed in the GAC, the CLR will invoke your ResolveEventHandler
(s) to allow you to supply a valid assembly. Should you not supply a valid assembly, the CLR will throw an exception.
Hooking into MshtmlAssemblyHelper
Program.cs is boilerplate with one exception:
DaveChambers.MshtmlAssemblyHelper.Initialize();
and Initialize
does this:
AppDomain.CurrentDomain.AssemblyResolve +=
new ResolveEventHandler(MyResolveEventHandler);
which simply adds my method (MyResolveEventHandler
) to the list of callbacks. MyResolveEventHandler
simply checks that MSHTML is being requested, and if so, obtains the path of my known-good MSHTML assembly, loads it, and returns the assembly to the CLR:
static private Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
AssemblyInfo info = new AssemblyInfo(args.Name);
if (info.Name.ToLower() != MshtmlAssemblyName.ToLower())
return null;
string assemblyPath = MshtmlAssemblyHelper.Install();
Assembly assembly = Assembly.LoadFrom(assemblyPath);
return assembly;
}
The "special" stuff is performed by Install()
. I previously mentioned "my known-good MSHTML assembly", and I now need to expand on that. Because the most common reason for MyResolveEventHandler()
getting called is the absence of any copies of MSHTML on the system, in order to load a copy, you must first install it on the local system. My (low-tech) solution was to place a copy of my MSHTML assembly (search your C drive for Microsoft.mshtml.dll) in a network folder and have MshtmlAssemblyHelper
copy the file from that directory to the local system, then install it via gacutil. That final step - installation with gacutil - is simply a convenience. You can load an uninstalled copy of MSHTML and the CLR will not complain. The benefit of installing it is that the next time your app runs, the CLR will find MSHTML in the GAC and thus won't need to call your ResolveEventhandler
.
Note that I use a mapped network drive to retrieve the installable DLL. My original app is an in-house tool; thus, using a net-drive-based retrieval mechanism was the quick and obvious choice. If your app were to be deployed outside your net, you'd want to implement some sort of Internet (e.g., HTTP) based DLL retrieval mechanism. I purposely omitted that functionality here as I wanted to focus on ResolveEventHandler
rather than the network stuff.
Testing the Code
As I mentioned above, VS2008 installs MSHTML, thus MyResolveEventHandler
will never be invoked on a dev system. You can, however, force the CLR to call MyResolveEventHandler
on your system by hacking the app.config entry:
<bindingRedirect oldVersion="1.0.0.0-7.9.9999.9" newVersion="8.0.0.0" />
This causes the CLR to consider your valid MSHTML assembly to be downrev, thus forcing MyResolveEventHandler
to be invoked. You'll then return MSHTML v7.0.3300.0 and the CLR won't like that (it's downrev), but it does allow you to debug your ResolveEventHandler
. A copy of the above test bindingRedirect
is in the demo's app.config, commented out.
History
- 27th August, 2009: Initial version