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