使用异步任务并行使用 WebView2

5

我有一个简单的默认Windows桌面表单Form1,上面有一个按钮btn_Go,作为测试。
我想要同时运行多个WebView2实例,并处理呈现页面的html代码。 为了并行运行WebView2,我使用SemaphoreSlim(设置为并行2)。另一个SemaphoreSlim用于等待WebView2呈现文档(带有一些时间延迟)。

但是我的代码在await webBrowser.EnsureCoreWebView2Async();处失败。 调试器中来自WebView2实例webBrowser的内部异常是:

{"Cannot change thread mode after it is set. (Exception from HRESULT: 0x80010106 (RPC_E_CHANGED_MODE))"} System.Exception {System.Runtime.InteropServices.COMException}

如何调用多个WebView2并行处理所有url?

完整的演示代码:

using Microsoft.Web.WebView2.WinForms;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WebView2Test
{
  public partial class Form1 : Form
  {
    public Form1() { InitializeComponent(); }

    private void btn_Go_Click(object sender, EventArgs e) { Start(); }

    private async void Start()
    {
        var urls = new List<string>() { "https://www.google.com/search?q=test1", "https://www.google.com/search?q=test2", "https://www.google.com/search?q=test3" };
        var tasks = new List<Task>();
        var semaphoreSlim = new SemaphoreSlim(2);
        foreach (var url in urls)
        {
            await semaphoreSlim.WaitAsync();
            tasks.Add(
            Task.Run(async () => {
                try { await StartBrowser(url); }
                finally { semaphoreSlim.Release(); }
            }));
        }
        await Task.WhenAll(tasks);
    }

    public async Task<bool> StartBrowser(string url)
    {
        SemaphoreSlim semaphore = new System.Threading.SemaphoreSlim(0, 1);

        System.Timers.Timer wait = new System.Timers.Timer();
        wait.Interval = 500;
        wait.Elapsed += (s, e) =>
        {
            semaphore.Release();
        };
        WebView2 webBrowser = new WebView2();
        webBrowser.NavigationCompleted += (s, e) =>
        {
            if (wait.Enabled) { wait.Stop(); }
            wait.Start();
        };
        await webBrowser.EnsureCoreWebView2Async();
        webBrowser.CoreWebView2.Navigate(url);

        await semaphore.WaitAsync();
        if (wait.Enabled) { wait.Stop(); }

        var html = await webBrowser.CoreWebView2.ExecuteScriptAsync("document.documentElement.outerHTML");
        return html.Length > 10;
    }
  }
}

我已经安装了WebView2 运行库

-- 测试

在主线程中准备 WebView2 并将其发送到子线程。

我已经尝试过在主线程的Start方法中创建WebView2列表var bro = new List<WebView2>() { new WebView2(), new WebView2(), new WebView2() };,并将 WebView2 实例发送到await StartBrowser(bro[index], url);,但结果仍然出现相同的错误。


你可能会觉得这很有用:在任务上设置ApartmentStateWebView2组件在其生命周期内可能不喜欢被多个线程访问。 - Theodor Zoulias
1个回答

6
您可以尝试使用下面自定义的TaskRunSTA方法替换您代码中的Task.Run
public static Task TaskRunSTA(Func<Task> action)
{
    var tcs = new TaskCompletionSource<object>(
        TaskCreationOptions.RunContinuationsAsynchronously);
    var thread = new Thread(() =>
    {
        Application.Idle += Application_Idle;
        Application.Run();
    });
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    return tcs.Task;

    async void Application_Idle(object sender, EventArgs e)
    {
        Application.Idle -= Application_Idle;
        try
        {
            await action();
            tcs.SetResult(null);
        }
        catch (Exception ex) { tcs.SetException(ex); }
        Application.ExitThread();
    }
}

这个方法启动一个新的STA线程,并在此线程内运行一个专用的应用程序消息循环。您传递作为参数的异步委托将在该消息循环中运行,安装了适当的同步上下文。只要您的异步委托不包含使用.ConfigureAwait(false)配置的任何await,包括由WebView2组件引发的事件,都应该在这个线程上运行。


注意: 老实说,我不知道处理Application.Idle事件的第一次出现是否是在消息循环中嵌入自定义代码的最佳方式,但它似乎相当有效。值得注意的是,此事件附加和分离于内部类ThreadContext (源代码),并且该类每个线程都有专用实例。因此,每个线程都会接收与在该线程上运行的消息循环相关联的Idle事件。换句话说,没有收到来自其他不相关消息循环的事件的风险,这些消息循环在另一个线程上并发运行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接