C# (托管代码) 和 JavaScript (非托管代码) 通过一个名为 ObjectForScripting 的桥梁对象进行双向调用。

vc webbrowser 网页通信
(图片来源网络,侵删)

核心原理

WebBrowser 控件内部使用的是 IE 的渲染引擎(在旧版 .NET 中)或 Edge 的旧版渲染引擎(在较新 .NET Framework 版本中),它提供了一种机制,允许你将 .NET 的对象暴露给网页中的 JavaScript,同时也可以在 C# 中执行网页里的 JavaScript 函数。

这个通信过程是异步的。

准备工作:设置 ObjectForScripting

这是实现 C# -> JavaScript 通信的关键一步。

  1. 创建一个可被 COM 识别的 C# 类 这个类必须被标记为 [ComVisible(true)],并且必须有一个 [ComVisible(true)] 的公共构造函数,这个类会包含一些公共方法,这些方法就是网页可以调用的。

    vc webbrowser 网页通信
    (图片来源网络,侵删)
    using System;
    using System.Runtime.InteropServices; // 需要引入这个命名空间
    [ComVisible(true)]
    public class ScriptingObject
    {
        // 公共方法,可以被 JavaScript 调用
        public void ShowMessageFromJS(string message)
        {
            // 在 C# 端处理从 JS 传来的数据
            MessageBox.Show($"从网页收到了消息: {message}");
        }
        public int AddNumbers(int a, int b)
        {
            return a + b;
        }
    }
  2. 将此类实例赋值给 WebBrowser 控件的 ObjectForScripting 属性 通常在窗体的构造函数或 Load 事件中完成。

    public partial class MainForm : Form
    {
        private ScriptingObject scriptingObject;
        public MainForm()
        {
            InitializeComponent();
            // 确保网页加载完成后再进行交互
            this.webBrowser1.DocumentCompleted += WebBrowser1_DocumentCompleted;
        }
        private void MainForm_Load(object sender, EventArgs e)
        {
            // 创建桥梁对象
            scriptingObject = new ScriptingObject();
            // 将对象暴露给网页
            this.webBrowser1.ObjectForScripting = scriptingObject;
            // 加载本地 HTML 文件(推荐这种方式进行测试)
            this.webBrowser1.Navigate("file:///" + Application.StartupPath + "/index.html");
        }
        private void WebBrowser1_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
        {
            // 网页加载完成后,可以在这里执行一些初始化操作
            // 调用 JS 函数
            this.webBrowser1.Document.InvokeScript("initializeCSharp");
        }
    }

通信方式详解

C# 调用 JavaScript (C# -> JS)

使用 WebBrowser.Document.InvokeScript(string methodName, object[] args) 方法。

  • methodName: 要调用的 JavaScript 函数名。
  • args: 传递给 JavaScript 函数的参数数组(可选)。

示例:

C# 代码:

vc webbrowser 网页通信
(图片来源网络,侵删)
// 在某个按钮的点击事件中
private void btnCallJS_Click(object sender, EventArgs e)
{
    // 获取网页文档
    HtmlDocument doc = this.webBrowser1.Document;
    // 调用无参数的 JS 函数
    doc.InvokeScript("showAlertFromCSharp");
    // 调用带参数的 JS 函数
    string message = "你好,来自 C# 的问候!";
    doc.InvokeScript("updatePageContent", new object[] { message });
}

JavaScript 代码 (index.html):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">WebBrowser 通信测试</title>
    <style>
        body { font-family: sans-serif; }
        #output { border: 1px solid #ccc; padding: 10px; margin-top: 10px; }
    </style>
</head>
<body>
    <h1>WebBrowser 通信测试页</h1>
    <button onclick="callCSharpFunction()">调用 C# 方法</button>
    <div id="output">等待通信...</div>
    <script>
        // C# 可以调用的函数
        function showAlertFromCSharp() {
            alert("Hello from C#!");
        }
        function updatePageContent(message) {
            document.getElementById('output').innerText = message;
        }
        // C# 在 DocumentCompleted 事件中调用的函数
        function initializeCSharp() {
            console.log("C# 已初始化,可以开始通信了。");
        }
        // 供网页按钮调用的函数,该函数会调用 C#
        function callCSharpFunction() {
            // window.external 是 C# 暴露给 JS 的默认对象
            if (window.external) {
                // 调用 C# ScriptingObject 中定义的方法
                window.external.ShowMessageFromJS("你好,我是网页!");
                var result = window.external.AddNumbers(10, 25);
                document.getElementById('output').innerText = "C# 计算结果: " + result;
            } else {
                alert("无法连接到 C# 宿主。");
            }
        }
    </script>
</body>
</html>

JavaScript 调用 C# (JS -> C#)

这是通过 ObjectForScripting 实现的,在 JavaScript 中,window.external 对象就代表了你在 C# 中赋值给 ObjectForScripting 的那个实例。

JavaScript 代码:

// 在 JS 中,通过 window.external 访问 C# 对象
function callCSharpFunction() {
    if (window.external) {
        // 调用 C# 类的公共方法
        window.external.ShowMessageFromJS("你好,我是网页!");
        var result = window.external.AddNumbers(10, 25);
        document.getElementById('output').innerText = "C# 计算结果: " + result;
    } else {
        alert("无法连接到 C# 宿主。");
    }
}

C# 代码 (ScriptingObject.cs):

[ComVisible(true)]
public class ScriptingObject
{
    // 公共方法,可以被 JavaScript 调用
    public void ShowMessageFromJS(string message)
    {
        // 在 C# 端处理从 JS 传来的数据
        // 注意:这里不能直接操作 UI,因为 JS 调用是异步的,
        // 可能发生在非 UI 线程上。
        // 正确做法是使用 Control.Invoke
        this.webBrowser1.Invoke((MethodInvoker)delegate {
            MessageBox.Show($"从网页收到了消息: {message}");
        });
    }
    public int AddNumbers(int a, int b)
    {
        return a + b;
    }
}

重要提示:线程安全 WebBrowser 控件及其相关的 HtmlDocument 不是线程安全的,当 InvokeScriptObjectForScripting 中的方法被调用时,它们可能运行在后台线程,如果这些方法需要更新 UI(在窗体上显示消息),必须使用 Control.InvokeControl.BeginInvoke 将 UI 更新操作调度到 UI 线程。


完整工作流程总结

  1. 项目设置:

    • 在 Visual Studio 中创建一个 "Windows 窗体应用 (.NET Framework)" 项目。
    • 从工具箱中拖拽一个 WebBrowser 控件到窗体上。
    • 拖拽一个 Button 控件用于测试 C# 调用 JS。
  2. 创建 C# 桥梁类 (ScriptingObject.cs):

    • 创建一个新类,标记为 [ComVisible(true)]
    • 在类中定义 public 方法,这些方法将被 JS 调用。
    • 在方法内部,如果需要更新 UI,使用 this.Invoke
  3. 窗体代码 (MainForm.cs):

    • 在窗体加载事件 (Form_Load) 中:
      • 实例化 ScriptingObject
      • 将实例赋值给 webBrowser1.ObjectForScripting
      • 使用 webBrowser1.Navigate() 加载你的 HTML 文件。
    • WebBrowser.DocumentCompleted 事件添加处理程序,用于在页面加载完成后执行初始化或调用 JS。
    • 在某个事件(如按钮点击)中,使用 webBrowser1.Document.InvokeScript() 来调用 JS 函数。
  4. 创建 HTML 文件 (index.html):

    • 将此文件添加到你的项目中,并设置其 "复制到输出目录" 属性为 "如果较新则复制"。
    • 在 HTML 的 <script> 标签中:
      • 定义一些函数,供 C# 调用。
      • 使用 window.external 来调用 C# ScriptingObject 中定义的方法。

重要注意事项和最佳实践

  1. 安全风险:

    • 绝对不要 从不受信任的来源加载网页,因为这意味着你将完全信任该网页执行的任何 C# 代码,这是一个巨大的安全漏洞。ObjectForScripting 相当于给了网页代码管理员权限。
    • 始终加载本地或你完全信任的 HTML 文件。
  2. 异步性:

    • 所有通信都是异步的,不要期望 InvokeScript 或 JS 调用 C# 方法后会立即得到结果并继续执行,如果需要返回值,InvokeScript 会返回它,但 JS 调用 C# 的返回值则无法直接获取(除非通过其他方式,如再次调用 JS 函数)。
  3. 调试:

    • 调试 C# 调用 JS: 在 C# 代码中设置断点,然后执行,JS 代码出错,InvokeScript 可能会抛出异常。
    • 调试 JS 调用 C#: 在 C# 的 ScriptingObject 方法中设置断点,在网页的 JavaScript 代码中,可以使用 console.log() 输出信息,但这些信息不会在 Visual Studio 的输出窗口中显示,最简单的方法是在 C# 方法中用 MessageBoxDebug.WriteLine 来确认是否被调用。
  4. 现代化替代方案:

    • WebBrowser 控件基于旧的 IE/EdgeHTML 引擎,已过时,性能和兼容性都有问题。
    • 对于新的 .NET 项目(如 .NET 5/6/7/8),强烈推荐使用 WebView2 控件WebView2 基于强大的 Chromium 引擎,性能更好,更安全,并且提供了更现代、更强大的通信机制(如 CoreWebView2PostWebMessageAsJsonWebMessageReceived 事件),是未来的发展方向。