深入理解.NET中的WebBrowser控件与JavaScript交互

.NET环境中,使用WebBrowser控件来显示简单的网页是相对直接的,但存在一些限制。尽管市场上出现了越来越多的嵌入Web浏览器选择,但WebBrowser是微软唯一支持的。本文将探讨如何通过Scripting Bridge技术实现与JavaScript代码的紧密耦合,并解决一些常见的问题。

Scripting Bridge的设计与实现

Scripting Bridge的核心是客户端调用门(call gate),它负责解码来自宿主的请求,调用实际的JavaScript函数,并将结果以有用的方式返回给宿主。此外,调用门还需要在发生错误时向宿主提供基本的反馈。它构成了宿主和客户端之间的粘合剂。

.NET代码中,可以通过以下方式初始化Scripting Bridge:

using Vereyon.Windows; public partial class ExampleForm : Form { public ScriptingBridge Bridge { get; private set; } public ExampleForm() { InitializeComponent(); Bridge = new ScriptingBridge(webBrowser, true); } }

假设网页包含以下JavaScript部分:

<script type="text/javascript"> var myObject = { myFunction: function(data) { alert(data.message); return { id: 12356, date: new Date(), title: 'Some string' }; } } </script>

可以从.NET代码中调用myObject.myFunction()函数,如下所示:

private void scriptingButton_Click(object sender, EventArgs e) { var parameter = new ScriptingParameterData { Message = "Test message" }; var data = Bridge.InvokeFunction<ScriptingReturnData>("myObject.myFunction", parameter); }

其中ScriptingParameterData类定义如下:

public class ScriptingParameterData { public string Message { get; set; } }

如何知道Scripting Bridge是否已初始化?

ScriptingBridge对象提供了一个属性和一个事件来实现此目的:

public class ScriptingBridge { [ComVisible(false)] public bool IsInitialized { get; private set; } public event EventHandler Initialized; }

如何绕过ScriptingBridge提供额外服务?

由于只能设置一次ScriptingObject,并且需要将其设置为ScriptingBridge实例才能使桥接工作,因此没有直接的方法将额外的服务暴露给网页。一个简单的解决方案是扩展ScriptingObject,并从派生类中暴露服务。

这是WebBrowser用户经常问的问题,关于控制这个版本已经有很多讨论。Scripting Bridge会在初始化后神奇地告诉历史记录中的位置:

public class ScriptingBridge { [ComVisible(false)] public string DocumentMode { get; private set; } }

调用门是Scripting Bridge的核心。调用门的任务是解码来自宿主的请求,调用实际的JavaScript函数,并将结果以有用的方式返回给宿主。此外,调用门还需要在发生错误时向宿主提供基本的反馈。它构成了宿主和客户端之间的粘合剂。

调用门必须在页面加载时向Scripting Bridge表明其存在。为此,Scripting Bridge提供了以下COM可见函数:

[ComVisible(true)] public bool RegisterClient(string args)

参数args包含一个JSON字符串,包含基本配置和客户端环境的信息。最重要的参数是调用门在全局window对象上可用的名称。这将是Document.InvokeScript()在幕后调用的函数名称。

WebBrowser控件提供的Document.InvokeScript()方法有一个主要缺点,它只能调用全局上下文中的函数。它不允许调用对象的任何成员函数。

为了克服这个问题,调用门需要解析对象的上下文。通过以“namespace.object.function”的格式传递目标函数,并在点上分割,调用门获得以下字符串列表:{“namespace”,“object”,“function”}。调用门假设最后一部分是函数的名称,任何前面的部分都定义了上下文。上下文通过首先在全局上下文中查找第一块“namespace”,然后查找“namespace”的成员“object”,依此类推来解析。在代码中看起来如下:

var context = window; var namespace = method.split('.'); var func = namespace.pop(); for (var i = 0; i < namespace.length; i++) { context = context[namespace[i]]; if (context == undefined) { return JSON.stringify({ success: false, error: namespace.slice(0, i + 1).join('.') + ' is undefined.' }); } } result = context[func](args);

鉴于Scripting Bridge的设计,需要确保Scripting Bridge的客户端在网页中可用。一种方法是使用传统手段在每个网页中包含代码。此外,当在某些较旧的兼容性模式下运行Internet Explorer时,JSON对象可能不可用。

为了不必在每个查看的页面中包含这些脚本,研究了一种通过Scripting Bridge本身注入这些脚本的方法。事实证明,使用所需的脚本源代码作为参数调用JavaScripteval()函数效果很好。

使用Document.InvokeScript()函数传递对象是完全可能的。只需要确保传递的所有内容都对COM可见,使用ComVisible属性,并确保任何不应该从JavaScript代码调用的成员方法都被适当地隐藏。

不想处理这个问题,所以让Scripting Bridge将任何传递的参数序列化为JSON。额外的好处是,可以在调用Document.InvokeScript()时不必总是将参数放在对象数组中。

当从客户端JavaScript返回对象到宿主端时,这些数据将作为COM对象暴露,对于任何非原始数据类型都是如此。虽然完全可能,但许多程序员发现使用COM对象非常具有挑战性,并且可能很繁琐。

希望能够在没有任何麻烦的情况下从客户端JavaScript传递任意数据到宿主端。为了实现这一点,决定让调用门将任何返回的数据序列化为JSON,并在宿主端再次反序列化这些数据。通过使这种实现通用,它允许直接反序列化到强类型数据模型,而不需要进一步的映射。

Document.InvokeScript()有一个烦人的特性,即在发生错误时返回null。这使得无法区分发生错误和函数简单地返回null或无返回值。例如,以下代码的正确执行与发生错误无法区分:

function voidReturn() { // Do some stuff return; }

为了处理这个问题,客户端的scripting bridge将每个函数调用结果包装在一个明确指示是否发生错误的对象中。如果发生无法恢复的错误,Document.InvokeScript()将返回null,宿主端的scripting bridge可以判断确实出了什么问题。

正如提到的,ScriptingBridge利用JSON在客户端和宿主端之间传递内部数据结构。使用Newtonsoft的JSON.Net库来执行宿主端的序列化和反序列化。

通常,从JSON到.NET对象的映射并不完全直接。想想确切的对象成员命名约定。使用驼峰式大小写,还是大写,这在JavaScript和.NET代码中是否相等?为了避免强制用户使用任何特定的约定,通过JsonSerializerSettings属性公开了有效载荷数据的JSON序列化设置。

这一切真的有效吗?是的,请查看包含的Vereyon.Windows.WebBrowser.Tests项目,了解基于xUnit的单元测试。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485