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.
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.
IWebBrowser2
and
DWebBrowserEvents2
. Some knowledge about COM Interop and interfaces is also required.
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?
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.
DWebBrowserEvents2
DownloadComplete
event DownloadComplete
fires, subscribe to the HtmlWindow.Error
event Handled
property to true
to suppress the script error 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:
NewWindow3
clearly is very interesting for this purpose. To use this event,
DWebBrowserEvents2
needs to be implemented.
DWebBrowserEvents2
<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.
AttachInterfaces
and DetachInterfaces
IWebBrowser2
interface object Application
that exposes the Application
property of the interface DWebBrowserEvents2
interface NewWindow2
and/or NewWindow3
events ppDisp
to the Application
property of the new instance 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!)
WndProc
WM_PARENTNOTIFY
WM_DESTROY
Quit
, in the example) IWebBrowser2
, for obtaining the Application
property of the browser DWebBrowserEvents2
for firing events 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.
/// <summary> /// An extended version of the <see cref="WebBrowser"/> control. /// </summary> public class ExtendedWebBrowser : System.Windows.Forms.WebBrowser { private UnsafeNativeMethods.IWebBrowser2 axIWebBrowser2; /// <summary> /// This method supports the .NET Framework /// infrastructure and is not intended /// to be used directly from your code. /// Called by the control when the underlying /// ActiveX control is created. /// </summary> /// <param name="nativeActiveXObject"></param> [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] protected override void AttachInterfaces(object nativeActiveXObject) { this.axIWebBrowser2 = (UnsafeNativeMethods.IWebBrowser2)nativeActiveXObject; base.AttachInterfaces(nativeActiveXObject); } /// <summary> /// This method supports the .NET Framework infrastructure /// and is not intended to be used directly from your code. /// Called by the control when the underlying /// ActiveX control is discarded. /// </summary> [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] protected override void DetachInterfaces() { this.axIWebBrowser2 = null; base.DetachInterfaces(); } ... }
Application
property.
/// <summary> /// Returns the automation object for the web browser /// </summary> public object Application { get { return axIWebBrowser2.Application; } }
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) 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.
CreateSink()
and
DetachSink()
. When adding this all up, we get something like this (note that some code has been cut for readability):
/// <summary> /// An extended version of the <see cref="WebBrowser"/> control. /// </summary> public class ExtendedWebBrowser : System.Windows.Forms.WebBrowser { // ... (More code here) System.Windows.Forms.AxHost.ConnectionPointCookie cookie; WebBrowserExtendedEvents events; /// <summary> /// This method will be called to give /// you a chance to create your own event sink /// </summary> [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] protected override void CreateSink() { // Make sure to call the base class or the normal events won't fire base.CreateSink(); events = new WebBrowserExtendedEvents(this); cookie = new AxHost.ConnectionPointCookie(this.ActiveXInstance, events, typeof(UnsafeNativeMethods.DWebBrowserEvents2)); } /// <summary> /// Detaches the event sink /// </summary> [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] protected override void DetachSink() { if (null != cookie) { cookie.Disconnect(); cookie = null; } } /// <summary> /// Fires when downloading of a document begins /// </summary> public event EventHandler Downloading; /// <summary> /// Raises the <see cref="Downloading"/> event /// </summary> /// <param name="e">Empty <see cref="EventArgs"/></param> /// <remarks> /// You could start an animation /// or a notification that downloading is starting /// </remarks> protected void OnDownloading(EventArgs e) { if (Downloading != null) Downloading(this, e); } // ... (More code here) #region The Implementation of DWebBrowserEvents2 for firing extra events //This class will capture events from the WebBrowser class WebBrowserExtendedEvents : UnsafeNativeMethods.DWebBrowserEvents2 { public WebBrowserExtendedEvents() { } ExtendedWebBrowser _Browser; public WebBrowserExtendedEvents(ExtendedWebBrowser browser) { _Browser = browser; } #region DWebBrowserEvents2 Members // ... (More code here) public void DownloadBegin() { _Browser.OnDownloading(EventArgs.Empty); } public void DownloadComplete() { _Browser.OnDownloadComplete(EventArgs.Empty); } // ... (More code here) #endregion } #endregion }
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; // Here's the new DownloadComplete event _browser.DownloadComplete += new EventHandler(_browser_DownloadComplete); // Some more code here... this.containerPanel.Controls.Add(_browser); // Some more code here... } void _browser_DownloadComplete(object sender, EventArgs e) { // Check wheter the document is available (it should be) if (this.WebBrowser.Document != null) // Subscribe to the Error event this.WebBrowser.Document.Window.Error += new HtmlElementErrorEventHandler(Window_Error); } void Window_Error(object sender, HtmlElementErrorEventArgs e) { // We got a script error, record it ScriptErrorManager.Instance.RegisterScriptError(e.Url, e.Description, e.LineNumber); // Let the browser know we handled this error. e.Handled = true; } // Some more code here }
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) { // Here we do the pop-up blocker work // Note that in Windows 2000 or lower this event will fire, but the // event arguments will not contain any useful information // for blocking pop-ups. // There are 4 filter levels. // None: Allow all pop-ups // Low: Allow pop-ups from secure sites // Medium: Block most pop-ups // High: Block all pop-ups (Use Ctrl to override) // We need the instance of the main form, // because this holds the instance // to the WindowManager. MainForm mf = GetMainFormFromControl(sender as Control); if (mf == null) return; // Allow a popup when there is no information // available or when the Ctrl key is pressed bool allowPopup = (e.NavigationContext == UrlContext.None) || ((e.NavigationContext & UrlContext.OverrideKey) == UrlContext.OverrideKey); if (!allowPopup) { // Give None, Low & Medium still a chance. switch (SettingsHelper.Current.FilterLevel) { case PopupBlockerFilterLevel.None: allowPopup = true; break; case PopupBlockerFilterLevel.Low: // See if this is a secure site if (this.WebBrowser.EncryptionLevel != WebBrowserEncryptionLevel.Insecure) allowPopup = true; else // Not a secure site, handle this like the medium filter goto case PopupBlockerFilterLevel.Medium; break; case PopupBlockerFilterLevel.Medium: // This is the most dificult one. // Only when the user first inited // and the new window is user inited if ((e.NavigationContext & UrlContext.UserFirstInited) == UrlContext.UserFirstInited && (e.NavigationContext & UrlContext.UserInited) == UrlContext.UserInited) allowPopup = true; break; } } if (allowPopup) { // Check wheter it's a HTML dialog box. // If so, allow the popup but do not open a new tab if (!((e.NavigationContext & UrlContext.HtmlDialog) == UrlContext.HtmlDialog)) { ExtendedWebBrowser ewb = mf.WindowManager.New(false); // The (in)famous application object e.AutomationObject = ewb.Application; } } else // Here you could notify the user that the pop-up was blocked e.Cancel = true; }
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 :)
Quit
event is fired, it's simply a matter of finding the right window or tab to close, and
Dispose
the instance.
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.
DWebBrowserEvents2
was in the bug list of Microsoft .NET 2.0. This has been used with modification. WndProc
was told by someone called "JoeBlow" on the MSDN forums, who had it derived from the MSKB article #253219. I am - born in The Netherlands - living in Chile together with my wife. - a Microsoft Certified Professional Developer on all 3 areas (Windows, Web and Enterprise) - an MCITP on Microsoft SQL Server 2005 (Database Administrator) - an active programmer for about 14 years. - a business owner, of a Dutch company called "The Wheel Automatisering" ([url]http://www.thewheel.nl[/url]) - a coder in C#, VB.Net and Managed C++. - someone who likes to share knowledge For fun I like to go out with my dogs, enjoy the sun or write some articles that I share with the community.
Click here to view Jeroen Landheer's online profile.
|