You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF WebView2中JS回调宿主对象时C#同步调用JS脚本的死锁问题求助

解决WebView2 WPF应用中JS回调宿主方法时同步调用JS的死锁问题

我之前也遇到过完全相同的场景——在WebView2的JS回调宿主C#方法时,尝试同步调用JS脚本导致死锁。先帮你理清死锁的根源,再给出两种可行的解决方案。

死锁原因分析

默认情况下,WebView2会把JS对宿主对象的调用调度到WPF的UI线程执行。当你在这个UI线程上下文中使用Dispatcher.PushFrame同步等待JS脚本执行结果时,会触发死锁:

  1. UI线程被PushFrame阻塞,无法处理WebView2的内部消息;
  2. WebView2的JS引擎正在等待宿主方法返回结果,而宿主方法的完成依赖于新的JS脚本执行结果;
  3. 新的JS脚本执行完成后,WebView2需要通过UI线程的消息循环通知C#,但UI线程已经被阻塞,无法处理这个通知,最终形成循环等待。

解决方案1:将宿主对象回调调度到后台线程(推荐)

通过配置WebView2的HostObjectInvokeHandler,让JS对宿主对象的调用在后台线程执行,这样同步等待JS脚本时就不会阻塞UI线程,也不会干扰WebView2的消息处理。

步骤1:初始化WebView2时配置调用处理器

private async void InitializeWebView2()
{
    await webView.EnsureCoreWebView2Async();
    
    // 注册宿主对象到JS环境
    var hostObject = new HostProxy(webView.CoreWebView2);
    webView.CoreWebView2.AddHostObjectToScript("appHost", hostObject);
    
    // 配置宿主对象调用的线程上下文:转到后台线程执行
    webView.CoreWebView2.Settings.HostObjectInvokeHandler = (sender, args) =>
    {
        Task.Run(() =>
        {
            args.CompletedSynchronously = false;
            try
            {
                // 执行宿主方法并返回结果
                var result = args.Invoke();
                args.SetResult(result);
            }
            catch (Exception ex)
            {
                args.SetException(ex);
            }
        });
    };
}

步骤2:在宿主对象中同步调用JS脚本

现在宿主方法在后台线程执行,直接同步等待ExecuteScriptAsync的结果即可,不会产生死锁:

public class HostProxy
{
    private readonly CoreWebView2 _webView;

    public HostProxy(CoreWebView2 webView)
    {
        _webView = webView;
    }

    // JS会调用这个方法
    public string OnSaveCallback()
    {
        // 同步执行JS脏检查脚本
        var dirtyCheckResult = _webView.ExecuteScriptAsync("window.performDirtyCheck()")
            .GetAwaiter()
            .GetResult();
        
        // 处理结果并返回给JS
        return dirtyCheckResult;
    }
}

解决方案2:在UI线程上下文避免嵌套Dispatcher循环

如果不想修改宿主对象的线程上下文,可以通过ConfigureAwait(false)让JS脚本的完成回调在后台线程执行,避免阻塞UI线程的消息循环:

public class HostProxy
{
    private readonly CoreWebView2 _webView;

    public HostProxy(CoreWebView2 webView)
    {
        _webView = webView;
    }

    public string OnSaveCallback()
    {
        var resetEvent = new ManualResetEventSlim(false);
        string scriptResult = null;
        Exception executionException = null;

        // 在UI线程发起JS脚本调用,但不让后续回调回到UI线程
        Application.Current.Dispatcher.Invoke(() =>
        {
            _webView.ExecuteScriptAsync("window.performDirtyCheck()")
                .ConfigureAwait(false) // 关键:避免回到UI线程
                .ContinueWith(t =>
                {
                    if (t.IsFaulted)
                    {
                        executionException = t.Exception;
                    }
                    else
                    {
                        scriptResult = t.Result;
                    }
                    resetEvent.Set();
                }, TaskContinuationOptions.ExecuteSynchronously);
        });

        // 等待脚本执行完成
        resetEvent.Wait();
        
        if (executionException != null)
        {
            throw new InvalidOperationException("JS脚本执行失败", executionException);
        }

        return scriptResult;
    }
}

注意事项

  • 方案1是最优解,因为它完全避免了UI线程阻塞,同时代码逻辑更清晰;
  • 如果你使用方案2,要确保ExecuteScriptAsync的回调不会依赖UI线程的资源,否则需要额外处理线程切换;
  • 不要在UI线程中直接调用ExecuteScriptAsync().Wait(),这会导致和Dispatcher.PushFrame一样的死锁问题。

内容的提问来源于stack exchange,提问作者Software Koder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 21:42:27