随由于ActiveX控件只能在单线程空间中运行,而Webbrower是ActiveX控件的一种,所以有会有如下几点要求:
(1)在异步的时候,Webbrower必须转移到单线程空间中进行创建和运行。在一些情况下,使用异步多线程会改变线程的状态,使Webbrower创建或者运行代码空间不是单线程空间,导致如下的错误,如图1:
图1
解决的方法:将创建或者执行的线程归入窗体的单线程空间中。使用“this.Invoke(()=>{...});”来进行解决。
注:这种问题还有一种表现形式,在代码执行的时候卡死或者创建的时候,窗体Show()不出来或者Show()出来一片空白。均有可能是由于这个问题造成的。
(2)还有一中情况是,当前执行的代码段就是不是单线程空间。在winform中创建,在Main()方法上本来就有声明“[STAThread]”即为单线程空间。所以在一些控制台程序中使用,要在创建或者执行有Webbrower的方法上标注标签“[STAThread]”,如下图2:
图2
还有另外一种做法就是使用System.Threading.Thread的时候,显示声明单线程单元,如下代码:
Thread thread = new Thread(() => Console.WriteLine("Hello Webbrower!"));
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
(3)还有切记不要将业务逻辑归入有Webbrower的窗体中做,而是应该直接在调用方的领域空间中完成。并且不要让归入的主窗体有时间非常长的主线程业务处理逻辑(主线程需要同步处理),不然会有业务逻辑执行完成之后,调用Invoke将线程归入主窗体进行处理,来刷新Webbrower,而主窗体中又有事务在处理,导致Invoke的处理事项一直在等待主窗体的任务执行完毕,造成Webbrower的窗体无法直接更新,造成假死。
解决的方法:Webbrower窗体不处理复杂的业务逻辑。业务逻辑让调用方异步进行,刷新的话让调用方回调处理Webbrower窗体刷新等操作。
这是我的测试工程
在Webbrower的使用过程中会涉及到COM互操作,就是一些底层的C++库的调用。只要在Webbrower承载的窗体类上增加特性“[ComVisibleAttribute(true)]”,否则在运行的时候会出现异常提示。如下图3:
图3
(1)禁用用户鼠标右键菜单,将IsWebBrowserContextMenuEnabled设置为false;
(2)禁用快捷键,将WebBrowerShortcutsEnabled设置为false;
(3)尽量不要使用忽略脚本错误提示设置,而是通过如下的方式来在前端进行捕获,并在后台进行日志等的异常处理,如下代码:
function TestMethod()
{
try
{
//可能会出现问题的代码
}
catch(e)
{
//前端的异常处理逻辑
window.external.ResolveException(e);//后台的异常处理方法
}
};
(4)禁用超链接,超链接有两种,一种是当前页面直接跳转,一种是在新窗口的打开:
<1>对于在窗口中直接跳转的,直接禁用AllowNavigation为false
<2>对于在新的窗口中打开的,我们要拦截webbrower的打开新窗体的事件:
private void webBrowser_NewWindow(object sender, CancelEventArgs e)
{
e.Cancel = true;
}
(5)禁用下拉列表,将AllowWebBrowserBrop设置为false;
(6)在一些做窗体的时候,可以禁用滚动条ScrollBarsEnabled为false。于此同时将Dock的填充属性设置为Fill;
(7)禁用鼠标刷新和右击,如果不禁用会导致右击或者按F5刷新导致页面一片空白:
//禁用鼠标刷新和右击
document.onkeydown = function (e) {
e = window.event || e;var keycode = e.keyCode || e.which;
if (keycode = 116) {
return false;
}
}
document.oncontextmenu = function (e) {
return false;
}
四. Webbrower内部网页使用注意与技巧 (Inner Html Page)
(1)使用网页最大兼容性,在head标签中加入,即最佳兼容性方案。因为webbrower默认用IE7的内核进行加载网页,通过这个声明强行把使用的IE设置为最高。有时候会显示canves的某某某属性不存在,或者莫名其妙的提示一个js的“缺少标识符、字符串或数字”的错误,一般是由于IE使用的内核太低导致的。用这句话来提高IE的运行版本,使其能兼容这些html或js的新增的元素。
注:这句话要紧跟在head后面,并且前面不要有任何其他的标签,这是为了防止失效。这个声明同时也会解决一定显示布局的兼容性问题,如:可以解决界面布局可以被选中的问题,如下图4:
图4
(2)通过使用CSS来清除所有元素的边距:
*{
magin:0px;
padding:0px;
}
(3)禁止用户对内容的选择,比如刷黑文字来选择复制:
选择器{
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
注:这种方式会让文字在内的等等的元素,在webbrower中不能被选择复制。
*(4)禁止用户将图片拖放到其他的Webbrower中:
$(document).ready(function () {
document.ondragstart = function (event) {
return false;
};
});
注:这种方式是用来防止图片元素在不同的webbrower中被拖来拖去,使得页面被图片沾满之后出现意想不到的BUG。这边我找了一个例子,如下图5:
图5
*(5)不要轻易使用标签,如果一定要使用来简单的制造“下划线”的效果,那么要将它的href点击特性进行禁用。如:内容部分进行禁用(当然有其他的禁用方式,只要可以做到禁用就行)。这是因为,如果在不禁用的情况下或者简单写成href="#"或者href=""的情况下,都会出现使webbrower重载网页,即在C#后台调用一次webbrower的DocumentCompleted事件,导致出现很难发现的隐藏Bug。如下图6,进行禁用连接跳转:
图6
(6)对于webbrower载入网页的速度问题,尽量做到在不用动态加载的部分,直接写成静态的html标签和样式,在网页Navigate的时候直接初始化掉。对于动态部分,比如文字内容等,再在DocumentCompleted的时候进行动态的注入。这样会减少加载网页的时候出现“白色”(没有内容)的时间。
*(1)IE11在webbrower加载gif图片多次打开窗体,或者从这个加载窗体打开新的窗体之后再退回到这个窗体的时候。会有gif不动的情况。但在IE9和IE10中就不会有这个问题,可以尝试使用WPF中的Webbrower控件解决gif不动的问题,就是在winform中加入一个WPF的用户控件,里面承载一个Webbroser。
*(2)在使用WPF的webbrower时,有时候在定了两个div元素后,在webbrower中加载完毕后,用鼠标同时跨div,选中两个div中的部分内容,会报出一个神奇的C++底层运行库的错误问题,这个问题可能是由以下两个原因造成的:
<1>C++运行库没有安装,或者安装未安装完整;
<2>IE9内核的兼容问题(由于亲测IE10及以上版本都不会有这个问题);
(3)最后说下IE9以下的内核。IE8开始就不能很正常的在webbrower中运行网页了。会弹出各种错误。对于更加以下的版本,就完全没得用了。
所以综上所述,最好的使用的IE内核就是IE10了。在程序中,只要用户电脑中有IE10的版本,即使默认是IE11,也可以通过在软件运行的时候动态去强制使用IE10来处理webbrower网页的使用,也可以通过修改注册表来实现。
之前的写的这篇文章没有写这个,这个第五点是后面补充的。在此期间查看了网上的很多资料,有一篇文章说,在IE8以下的版本中,微软有一个Bug会造成webbrower内存泄漏,但是在IE及以上版本中不会有这个问题。
WebBrower控件 内存溢出(泄露)解决方案汇总
于是我在项目测试的时候使用了IE9以上的内核,但是居然还是有内存泄漏的问题。于是又看了好多网上的资料,说使用GC回收配合SetProcessWorkingSetSize来处理这个问题,但是尝试一下,发现还是有内存泄漏的问题。
用SetProcessWorkingSetSize来解决
由于SetProcessWorkingSetSize是把物理内存转换为虚拟内存来减少程序集的内存大小,但是还是会由于周而复始的Webbrower创建加载导致程序弹出内存不足的异常。重点是使用了GC.Colloct()没有任何的作用。
后面多次尝试后发现,在Winform的Webbrower中要进行先显示的Dispose()之后,即Webbrower.Dispose()之后,再进行GC.Collect(),才能将Webbrower占用的内存释放。与此同时,经过尝试后发现,对于WPF的Webbrower就算不显式的使用Dispose(),它的内存可以被GC释放:P。
所以只要在窗体的Closing事件中,加入Webbrower.Dispose()即可解决问题。CLR书上说了,要相信GC的垃圾回收算法和能力。
要说不同,其实WPF和Winform的东西真的是大同小异。这边我就讲一个注意吧,第一次我写WPF的时候,遇到的一个问题。
在WPF中绑定ObjectForScripting的对象与Winform中有所不同。在Winform中可以直接使用窗体来作为绑定的对象,即“this.webBrowser.ObjectForScripting = this;”,但是在WPF中必须创建一个代理对象(随便建一个类对象就可以)来进行绑定。
我把上面“神一般”的Bug用“*”标注出来了,这两个问题我阅读了很多的国外文章才总结出来的。在国内用百度根本搜到的都是一知半解的东西,希望可以给自己和大家在工作中有解决方法的思路。如果有什么说的不对的,可以下面留言,我会及时纠正,如果真的帮到了,希望可以关注我一下,或者转发一下。谢谢大家。2018年6月8日,晚。