目录
1、 简介
2、 目标、挑战和解决方案
捕捉脚本错误
过滤弹出窗口
添加多标签页或多窗口浏览功能
当一个窗口是由脚本关闭时,需要确认
3、 创建webbrowser扩展控件
实现 iwebbrowser2接口
实现 dwebbrowserevents2接口
4、 使用该扩展控件
捕捉脚本错误
过滤弹出窗口并添加多标签页或多窗口浏览功能
使用退出事件
1、简介
在.net 2.0的system.windows.forms命名空间中新增了webbrowser控件,该控件本身是非常有用的,但是它没有提供在某些情况下需要的事件。这篇文章描述了如何扩展webbrowser控件并增加一些功能,例如:屏蔽弹出窗口、捕捉脚本错误以及捕捉新窗口并将其显示在多标签浏览窗口环境中。
在扩展webbrowser控件时,某些功能没有写入.net framework的帮助文件,不用理会“这个方法是用于支持.net基础架构的,不推荐直接用于您的代码中”的提示信息,我们可以创建一个实现iwebbrowser2接口的对象,并使用浏览器对象的全部功能,此外,使用dwebbrowserevents2接口可以向控件中添加事件。
我们假设你已经了解了iwebbrowser2接口和dwebbrowserevents2接口,对com的互操作和相关的接口知识也是需要了解的。
2、目标、挑战和解决方案
这个组件要实现的目标是:
用简洁的方式捕捉脚本错误
过滤弹出窗口
加入多标签页浏览或多窗口浏览功能
当窗口被脚本关闭时需要确认
这一节简要讲解实现这些目标所碰到的问题和相关的解决方案,下一节中会给出更多的代码细节。
捕捉脚本错误
webbrowser控件有一个scripterrorsuppressed属性,将这个属性设置为true时,该控件确实会比原来多做了一点事情,它不仅禁用了脚本出错的对话框,而且还禁用了登陆到需要用户证书的安全站点时出现的登陆对话框。但是假如我们仍然需要这个功能,或者我们想获得脚本出错的通知,或者我们想知道全部的脚本出错的细节时该怎么办呢?
脚本错误可以在htmlwindow.error事件中捕捉,这个事件会在脚本发生错误时触发并包含全部的错误细节信息,但是难点在于htmlwindow是需要通过htmldocument对象才能访问,而该对象并不是什么时候都有效,htmldocument对象只在navigated事件触发时才有效,而假如用户是按f5键刷新浏览器时呢,抱歉,navigated事件是不会触发的。在经过了很多的尝试后,我发现唯一可行的方法是使用并不是默认webbrowser控件一部分的downloadcomplete事件。
解决方案:
1. 实现dwebbrowserevents接口
2. 创建一个downloadcomplete事件
3. 当downloadcomplete事件触发时,订阅htmlwindow.error事件
4. 利用这个error事件来获得脚本出错的具体信息
5. 设置handled属性为true来阻止脚本出错
过滤弹出窗口
弹出窗口大部分情况都是不怎么受欢迎的或者是不适宜的,屏蔽这些弹出窗口需要一些额外的信息。当用户使用windows xp sp2或者windows 2003 sp1 或更高版本时,newwindow3事件可以提供这些辅助信息,假如这个事件没有触发,那么newwindows2事件会替代该事件。当newwindow3事件触发时,你可以检查以下内容:
l 是否是用户的操作才导致了新开窗口
l 用户是否按住了覆盖键(ctrl 键)
l 是否因为当一个窗口正在关闭才导致显示弹出窗口
l 获得将要打开窗口的url地址
l 更多...
使用newwindows3事件可以很明显的实现这个目的,假如要使用这个事件,就必须实现dwebbrowserevents2接口。
解决方案:
1. 实现dwebbrowserevents2接口
2. 创建一个新的事件和一个新的事件参数类
3. 执行这个事件并附带适当的信息
4. 当这个事件触发后,检查这次的导航是否需要取消
添加多标签页或多窗口浏览功能
多标签页方式浏览在目前似乎变得越来越流行,例如在ie7中,这就是一个新增功能。实现多标签页方式浏览的难点是,你需要在当脚本或者超链接创建一个新窗口的时候去创建相应的新的标签页或子窗口,除此之外,还需要解析出多窗口或者多标签页的窗口名称。(例如:<a href=”http://somesite” target=”somewindowname”/>)要实现这一点,一些自动化对象(如:newwindowx事件中的ppdisp和iwebbrowser2接口中的application)就需要从新开窗口传回到该事件中。而访问application属性需要获得iwebbrowser2接口的引用。
解决方案:
1. 重载attachinterfaces和detachinterfaces接口
2. 保存iwebbrowser2接口对象的引用
3. 创建一个application属性来暴露该接口中的application属性
4. 实现dwebbrowserevent2接口
5. 监听newwindows2和/或newwindow3事件
6. 当一个事件触发时,创建一个新的browser控件的实例
7. 将ppdisp事件参数指派给新实例的application属性
当一个窗口被脚本关闭时需要确认
当你在jscript中调用window.close()方法,webbrowser控件很可能出现假死。因为某种原因,他不能用于导航页面,也不能做其他任何事情。假如我们知道它什么时候发生可能会好一些。当它发生时会触发一系列的事件,但是这些事件没有给我们需要的信息。重载wndproc方法并检测父窗口是否通知该浏览器已经被销毁是唯一可行的解决方法(假如谁知道如何得到windowsclosing事件来实现这一点是更好的方法)
解决方案:
1. 重载wndproc方法
2. 检查wm_parentnotify消息
3. 检查wm_destriy参数
4. 假如检测到了上述的内容,则触发一个新的事件(这个事件在例子中称为quit)
3、创建webbrowser扩展组件
从上一节中,我们可以发现上述的所有内容都基本可以归结为两件事:
1. 实现一个iwebbrowser2类型的对象,从中获得application属性
2. 实现dwebbrowserevents2接口来触发事件
实现iwebbrowser2接口
using System; using System.Security; using System.Runtime.InteropServices; using System.Windows.Forms; using System.Security.Permissions; namespace MiniBrowser { ///<summary> /// An extended version of the <see cref="WebBrowser"/> control. ///</summary> 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(); } ///<summary> /// Returns the automation object for the web browser ///</summary> public object Application { get { return axIWebBrowser2.Application; } } 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); } ///<summary> /// Fires when downloading is completed ///</summary> ///<remarks> /// Here you could start monitoring for script errors. ///</remarks> public event EventHandler DownloadComplete; ///<summary> /// Raises the <see cref="DownloadComplete"/> event ///</summary> ///<param name="e">Empty <see cref="EventArgs"/></param> protected virtual void OnDownloadComplete(EventArgs e) { if (DownloadComplete != null) DownloadComplete(this, e); } …… } }
webbrowser控件有两个尚未公开的接口:attachinterfaces()和detachinterfaces()。这些方法用于获得iwebbrowser2接口的引用。
下一步,我们可以添加application属性。
///<summary> /// Returns the automation object for the web browser ///</summary> public object Application { get { return axIWebBrowser2.Application; } }
这个属性可以用来创建一个新窗口,并且当创建新窗口事件触发时将浏览器重定向到这个新窗口。
实现dwebbrowserevents2接口
在这个例子中实现了下列事件:
l newwindow2和newwindow3(用于屏蔽弹出窗口和创建新窗口)
l downloadbegin和downloadcomplete(用于捕捉脚本错误)
l beforenavigate2(用于在导航到一个页面前查看即将导航到的地址)
为了简洁的实现dwebbrowserevents接口,最好的方法是在组件中建立一个私有的嵌入类。这样,所有需要的事件都在一个地方并且轻易查找。当我们实例化这个类的时候,我们可以给调用者提供一个引用,利用该引用可以调用方法来触发我们需要的事件。
在组件的构造过程中并没有附带这些事件,而是稍微晚一点。这里有两个方法来实现它并且它们是可以重载的。它们是createsink()和detachsink()。当我们将这些都添加完以后,我们的代码会像下面这样(注重有些代码为了阅读方便而删掉了)
/// <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) THE IMPLEMENTATION OF DWEBBROWSEREVENTS2 FOR FIRING EXTRA EVENTS }
4、使用这个组件
上一节,我们创建了一个新的组件。现在,我们来使用这些新的事件并尽可能多的挖掘浏览器的功能。针对每一个目标,具体的解释如下:
捕捉脚本错误
在示例程序中,有一个工具窗口简单的显示了发生错误的列表并附带了错误的具体内容。一个单一实例类把握了脚本错误的信息并且当这个信息发生改变时通知所有订阅者,为了捕捉这些脚本错误,browsercontrol首先附加到downloadcomplete事件,其次它订阅了htmlwindow.error事件。当这个事件触发时,我们注册这个脚本错误并设置handled属性为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 }
过滤弹出窗口,并且增加多标签页或多窗口浏览功能
捕捉弹出窗口必须可以由用户来进行配置。为了示范的目的,我实现了四个级别,从不屏蔽任何窗口到屏蔽所有新窗口。下面的代码是browsercontorl的一部分,用来展现如何实现这一点。当一个新建窗口被答应后,示例程序展现了如何让新建窗口实现窗口名称的解决方案。
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的原因是编码设计规则不答应一个事件的名称以“before”或者“after”开头。“newwindowing”事件并没有在这一范围内。
使用quit事件
当quit事件触发时,我们只需要找到正确的窗口或者标签页将其关闭,并且”dispose”这个示例即可。