Contents
- Introduction
- The goals, challenges, and solutions
- Creating the extended WebBrowser component
- Using the component
- Conclusion
1: Introduction
.NET 2.0 has a new WebBrowser
control in the System.Windows.Forms
namespace. This control itself is very useful, but doesn't supply some events that might be needed in certain situations. This article explains how to extend the WebBrowser
control and add functionality for things like pop-up blocking, script error handling, and handling new windows in a tabbed browser environment.
For extending the WebBrowser
control, some features are not documented in the Help files of the .NET Framework. Not letting us be stopped by the "This method supports the .NET Framework infrastructure and is not intended to be used directly from your code." message, it is possible to create an object that implements IWebBrowser2
and use all the functionality of the underlying browser. Besides this, DWebBrowserEvents2
can be implemented for adding events to the control.
This article assumes that you have already some knowledge of the browser interfaces IWebBrowser2
and DWebBrowserEvents2
. Some knowledge about COM Interop and interfaces is also required.
2: The goals, challenges, and solutions
The goals of this component are:
- Handling script errors in a neat way
- Blocking unwanted pop-ups
- Enabling functionality for tabbed browsing or MDI browsing
- Making sure that a window is closed when it is closed by script
This section explains the problems associated with the goals and their solutions, in a short form. The next section goes more into the coding details.
Handling Script Errors
The WebBrowser
control has a ScriptErrorsSuppressed
property... Setting this property to true
does actually a bit more than it is supposed to. It not only disables the script error dialog, but also the dialog for logging on to a secure website with user certificates... What if we still want that functionality, or we would like to be notified when a script error has taken place, or we would like to know all the details about the script error?
The script error can be caught by the HtmlWindow.Error
event. This event fires whenever a script error occurs, with all the details. The challenge is that HtmlWindow
is to be accesed with the HtmlDocument
object, which is not always available. HtmlDocument
comes available as soon as the Navigated
event is fired. But what if the user refreshes the browser with F5? Sorry, the Navigated
event doesn't fire. After some testing, I found that the only reliable way was to use the DownloadComplete
event, which is not part of the default WebBrowser
control.
Solution:
- Implement
DWebBrowserEvents2
- Create a
DownloadComplete
event
- When
DownloadComplete
fires, subscribe to the HtmlWindow.Error
event
- Use the error event for obtaining the script error information
- Set the
Handled
property to true
to suppress the script error
Blocking unwanted pop-ups
Pop-ups are most of the time not very welcome, or could be inappropriate. To block these things, some additional information is needed. NewWindow3
gives this information when the user uses Windows XP SP2, or Windows 2003 SP1 or better. If this event is not fired, NewWindow2
takes its place. When NewWindow3
is fired, you can check:
- If the user initiated the action that leads to the new window
- If the user holds the override key (the Ctrl Key)
- If it is a pop-up displayed because of a window that is closing
- The URL that is going to be opened
- And more...
Using NewWindow3
clearly is very interesting for this purpose. To use this event, DWebBrowserEvents2
needs to be implemented.
Solution:
- Implement
DWebBrowserEvents2
- Create a new event and a new event arguments class
- Launch this event with the appropriate information
- After the event is fired, see if the navigation needs to be canceled
Enabling functionality for tabbed browsing or MDI browsing
Tabbed browsing seems to gain popularity these days. For Internet Explorer 7, this is one of the new features. The challenge in tabbed browsing is that you need to create a window when this is needed by scripts or links. Besides this, window name resolution should work over multiple windows or tabs. (For example: <A href="http://SomeSite" target="SomeWindowName"/>
) To achieve this, the automation object (called ppDisp
in the NewWindowX
event, and Application
in the IWebBrowser2
interface) should be passed from the new browser back to the event. To get access to the Application
property, it is needed to get a reference to the underlying IWebBrowser2
interface.
Solution:
- Override
AttachInterfaces
and DetachInterfaces
- Store a reference to the
IWebBrowser2
interface object
- Create a property called
Application
that exposes the Application
property of the interface
- Implement the
DWebBrowserEvents2
interface
- Listen for
NewWindow2
and/or NewWindow3
events
- Whenever an event is raised, create a new instance of the browser control
- Assign the event parameter
ppDisp
to the Application
property of the new instance
Making sure that a window is closed when it is closed by script
When you invoke window.close()
in JScript, it looks like the WebBrowser
control hangs. Somehow, it can't be used for navigation, nor can it be used for anything else. It would be nice if we know when this happens. There are several events that fire when this happens, but none of the events gives us actually the information needed. Overriding WndProc
and seeing if the parent is notified that the browser is destroyed, is the only reliable solution. (If someone knows how to get WindowClosing
to work, it would be nice here!)
Solution:
- Override
WndProc
- Check for the message
WM_PARENTNOTIFY
- Check for the parameter
WM_DESTROY
- If this is the case, fire a new event (this event is called
Quit
, in the example)
3: Creating the extended WebBrowser component
From the last section, we have seen that all of this basically comes down to two things:
- Implement an object of type
IWebBrowser2
, for obtaining the Application
property of the browser
- Implement
DWebBrowserEvents2
for firing events
Implementing IWebBrowser2
The WebBrowser
control has two methods that are undocumented: AttachInterfaces()
and DetachInterfaces()
. These methods need to be used when you want to obtain a reference to the IWebBrowser2
interface.
public class ExtendedWebBrowser : System.Windows.Forms.WebBrowser
{
private UnsafeNativeMethods.IWebBrowser2 axIWebBrowser2;
[PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")]
protected override void
AttachInterfaces(object nativeActiveXObject)
{
this.axIWebBrowser2 =
(UnsafeNativeMethods.IWebBrowser2)nativeActiveXObject;
base.AttachInterfaces(nativeActiveXObject);
}
[PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")]
protected override void DetachInterfaces()
{
this.axIWebBrowser2 = null;
base.DetachInterfaces();
}
...
}
Next, we can add the Application
property.
public object Application
{
get { return axIWebBrowser2.Application; }
}
This property can be used for creating a new window, and redirecting the browser to this new window, when a new window event is fired.
Implementing DWebBrowserEvents2
The following events are implemented in this sample:
NewWindow2
and NewWindow3
(for blocking pop-ups and creating new windows)
DownloadBegin
and DownloadComplete
(for handling script errors)
BeforeNavigate2
(if you want to see where you're going before even starting to get there)
To neatly implement DWebBrowserEvents2
, it is best to create a privately nested class in the component. This way, all the events that are needed are on one place and easy to find. When we instantiate this class, we provide a reference to the caller, whose methods can be invoked for raising the events we need.
Events are not attached at component construction, but a bit later. There are two methods here that provide this and can be overridden. These are CreateSink()
and DetachSink()
. When adding this all up, we get something like this (note that some code has been cut for readability):
public class ExtendedWebBrowser : System.Windows.Forms.WebBrowser
{
System.Windows.Forms.AxHost.ConnectionPointCookie cookie;
WebBrowserExtendedEvents events;
[PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")]
protected override void CreateSink()
{
base.CreateSink();
events = new WebBrowserExtendedEvents(this);
cookie = new AxHost.ConnectionPointCookie(this.ActiveXInstance,
events, typeof(UnsafeNativeMethods.DWebBrowserEvents2));
}
[PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")]
protected override void DetachSink()
{
if (null != cookie)
{
cookie.Disconnect();
cookie = null;
}
}
public event EventHandler Downloading;
protected void OnDownloading(EventArgs e)
{
if (Downloading != null)
Downloading(this, e);
}
#region The Implementation of DWebBrowserEvents2 for firing extra events
class WebBrowserExtendedEvents :
UnsafeNativeMethods.DWebBrowserEvents2
{
public WebBrowserExtendedEvents() { }
ExtendedWebBrowser _Browser;
public WebBrowserExtendedEvents(ExtendedWebBrowser
browser) { _Browser = browser; }
#region DWebBrowserEvents2 Members
public void DownloadBegin()
{
_Browser.OnDownloading(EventArgs.Empty);
}
public void DownloadComplete()
{
_Browser.OnDownloadComplete(EventArgs.Empty);
}
#endregion
}
#endregion
}
4: Using the component
In the last section, we created a new component. Now, it's time to use the new events and get the maximum functionality out of the browser. For each of the goals, the details are explained here.
Handling the script errors
In the sample application, there is a tool window that simply shows a list of errors that occured, with their details. A single-instance class holds the script errors' information and notifies the subscribers when this information has been changed. For handling these script errors, the BrowserControl
first attaches to the DownloadComplete
event, and next subscribes to the HtmlWindow.Error
event. When this event is fired, we register the script error and set the Handled
property to true
.
public partial class BrowserControl : UserControl
{
public BrowserControl()
{
InitializeComponent();
_browser = new ExtendedWebBrowser();
_browser.Dock = DockStyle.Fill;
_browser.DownloadComplete +=
new EventHandler(_browser_DownloadComplete);
this.containerPanel.Controls.Add(_browser);
}
void _browser_DownloadComplete(object sender, EventArgs e)
{
if (this.WebBrowser.Document != null)
this.WebBrowser.Document.Window.Error +=
new HtmlElementErrorEventHandler(Window_Error);
}
void Window_Error(object sender, HtmlElementErrorEventArgs e)
{
ScriptErrorManager.Instance.RegisterScriptError(e.Url,
e.Description, e.LineNumber);
e.Handled = true;
}
}
Blocking unwanted pop-ups, and enabling functionality for tabbed browsing or MDI browsing
Handling pop-ups should be user configurable. For the purpose of demonstration, I've implemented four levels, ranging from blocking nothing to blocking every new window. This code is part of the BrowserControl
, and shows how to do this. After the new window is allowed, the example shows how to let the new browser participate in the window name resolution.
void _browser_StartNewWindow(object sender,
BrowserExtendedNavigatingEventArgs e)
{
MainForm mf = GetMainFormFromControl(sender as Control);
if (mf == null)
return;
bool allowPopup = (e.NavigationContext == UrlContext.None)
|| ((e.NavigationContext &
UrlContext.OverrideKey) == UrlContext.OverrideKey);
if (!allowPopup)
{
switch (SettingsHelper.Current.FilterLevel)
{
case PopupBlockerFilterLevel.None:
allowPopup = true;
break;
case PopupBlockerFilterLevel.Low:
if (this.WebBrowser.EncryptionLevel !=
WebBrowserEncryptionLevel.Insecure)
allowPopup = true;
else
goto case PopupBlockerFilterLevel.Medium;
break;
case PopupBlockerFilterLevel.Medium:
if ((e.NavigationContext & UrlContext.UserFirstInited)
== UrlContext.UserFirstInited &&
(e.NavigationContext & UrlContext.UserInited)
== UrlContext.UserInited)
allowPopup = true;
break;
}
}
if (allowPopup)
{
if (!((e.NavigationContext &
UrlContext.HtmlDialog) == UrlContext.HtmlDialog))
{
ExtendedWebBrowser ewb = mf.WindowManager.New(false);
e.AutomationObject = ewb.Application;
}
}
else
e.Cancel = true;
}
The reason the event is called StartNewWindow
is that the code design guidelines do not allow an event to begin with "Before" or "After". "NewWindowing
" doesn't have the same kind of ring to it :)
Using the Quit event
When the Quit
event is fired, it's simply a matter of finding the right window or tab to close, and Dispose
the instance.
5: Conclusion
The WebBrowser
control is a good control for enabling web content in Windows applications. The additions in this article can be used to overcome the obstacles that developers face when they have no control over what web pages or other content the user might visit with their application. Hopefully, the next version of the .NET Framework will give us a little extra.
The sample application and source
The sample application is not a flashy UI, but it does demonstrate everything about this article. The code is commented, and hopefully gives enough information for helping you put your own solution together.
Acknowledgements
I would like to thank the following persons that made part of this article possible:
- The technique for
DWebBrowserEvents2
was in the bug list of Microsoft .NET 2.0. This has been used with modification.
- The technique for
WndProc
was told by someone called "JoeBlow" on the MSDN forums, who had it derived from the MSKB article #253219.
This is my first article on The Code Project. Please excuse me for my English. Thanks for reading! If you can add anything or have suggestions or tips, please post a message below.
License
This code is copyrighted by The Wheel Automatisering in The Netherlands. Some rights are reserved.
The code in this license may be used for any purpose, just let your users know where it came from, and share derived code under the same license as this one. Don't blame me if something goes wrong. More information can be found here.
If you wish to use and/or publish this in commercial closed-source applications, you have my consent. You may use this code under your own license when you do so.
History
- 27th of March 2006: First post of this article.