Tuesday, September 01, 2015

Eating the Browser cake

That side-project again. One of the primary outputs of the app is data and text collected from web sites in (mostly) HTML format. This can most simply be wrapped to create an HTML document and displayed in the WebBrowser control made available by Visual Studio. This works well up to a point – the WebBrowser control is a “wrapper” for the Internet Explorer COM object but only exposes a limited set of events and properties. There is also an issue with multi-threading – using the WebBrowser control makes it very difficult to update the UI thread from (say) a BackgroundWorker.

I was happy with the HTML presentation within the WebBrowser control but wanted to handle any user clicks on links by pushing the link off to the default browser on the relevant machine. An external link could require a whole host of support facilities not enabled by the WebBrowser control so this made sense as well as (probably) reflecting the expectations of any user.

So, how to push responsibility to those external links off to the user’s preferred browser. Turns out to be easy to handle in the Navigating event:

private void webViewer_Navigating(object sender, WebBrowserNavigatingEventArgs e) {     if (!hasStarted)     {         hasStarted = true;         return;     }     e.Cancel = true;     var startInfo = new ProcessStartInfo     {         FileName = e.Url.ToString()     };     Process.Start(startInfo); }

The bool pageShown is set false before loading the original page in the WebBrowser control and this is set true by the first Navigating event resulting from that page load. Subsequent Navigating events before the pageShown value is reset are pushed off to the default application (a web browser certainly) that handles URLs.

This worked fine up until the HTML content being displayed contained an <iframe> tag. While loading graphics and scripts from remote servers caused no issues filling the content of an <iframe> triggered a Navigating event. So I needed to know if a Navigating event was being triggered by an <iframe> requesting content as this may happen after the main content had loaded and the DocumentCompleted event fired (so I could not reliably re-set the pageShown Boolean within that event handler.

StackOverflow is your friend here of course. Questions like “possible to detect whether an iframe is loaded in the navigating event” hit the nail on the head. This starts a trail that initially leads to a March 2006 Code Project post by Jeroen Landheer http://www.codeproject.com/Articles/13598/Extended-NET-2-0-WebBrowser-Control#CreateIWebBrowser2 but also mentions the BeforeNavigate2 event thus exposed and the opportunity to insert a test into the BrowserExtendedNavigatingEventArgs class.

There is also this http://www.codeproject.com/Articles/18935/The-most-complete-C-Webbrowser-wrapper-control CodeProject article from May 2007 which is worthy of investigation if you need to follow a similar path.

It looks like the Windows System SHDocVw.dll exposes a whole range of additional interfaces and events for IE not exposed by the WebBrowser control but getting a handle on them takes a little effort to say the least – hence the projects to sub-class the WebBrowser in the two CodeProject articles mentioned.

Anyway – the only snag remaining is that the standard WebBrowser Navigating event fires before the BeforeNavigate2 event which is slightly counter-intuitive. So the trick is to move the functionality from the Navigating event into the WebBrowserExtendedEvents class BeforeNavigate2 event by adding a Boolean property to that class for the main form to notify the start (and end) of the main document loading.

As an aside, I did wonder if there were alternatives to the provided WebBrowser control. Turns out there have been projects to “wrap” WebKit and FireFox browsers but what looks like the most able and up-to-date project provides a wrapper for the Chrome browser and the GitHub repository is here https://github.com/cefsharp/CefSharp/wiki . However if you were expecting a nice Visual Studio control to “drop onto” a form then you might be disappointed. I think the idea is that you will want to wrap the functionality in a custom control of your own perhaps supporting tabs and other browser flummery. The simplest way to add the CefSharp control to your form is programmatically in the form class constructor thus:

private CefSharp.WinForms.ChromiumWebBrowser mBrowser; public Form1() {     InitializeComponent();     mBrowser = new CefSharp.WinForms.ChromiumWebBrowser("http://www.hanselman.com/blog/")     {         Dock = DockStyle.Fill,     };     this.Controls.Add(mBrowser); }
It works and seems very fast and responsive. The documentation currently takes the form of “read the code” so it was not immediately obvious I could solve my problems by switching the underlying browser but at first glance it did look possible. What stopped me spending too much time here was the requirement to build your project against the 32bit or 64 bit version. While there are fewer 32bit PCs out there it still represents an additional issue should this project ever be shared with others even informally.

No comments: