.NET桌面程序应用WebView2组件集成网页开发4 WebView2的线程模型


系列目录    

  WebView2控件基于组件对象模型(COM),必须在单线程单元(STA)线程上运行。

线程安全
  • WebView2必须在使用消息泵的UI线程上创建。所有回调都发生在该线程上,对WebView2的请求必须在该线程上完成。从另一个线程使用WebView2是不安全的。
  • 唯一的例外是CoreWebView2WebResourceRequest的Content属性。内容属性流是从后台线程读取的。流应该是灵活的,或者应该从后台STA创建,以防止UI线程的性能下降。
  • 对象属性是单线程的。例如,调用CoreWebView2CookieManager.CookiesAsync(null),从主线程以外的线程获取会成功(即返回cookie);但是在这样的调用之后尝试访问cookie的属性(例如c.Domain)将引发异常。

下面以真实项目案例(建筑工程施工图BIM人工智能审查系统)讲解WbView2控件如何实现与网页、宿主程序之间进行线程安全的互相通讯。

业务场景1

  项目的某个单体下有建筑、结构、给排水、电器、暖通 5个专业,【图纸信息】模型树中上传了4个模型,底部工具栏中有“查看智能审查结果”按钮。

(1)双击模型节点创建Tab页签,页签中使用WebView2控件加载网页,渲染对应的模型。

实现方式如下:

首先判断模型是否已经在Tab页中打开并加载,如果已经加载,则直接切换到对应的Tab页。如果未打开则创建新的Tab页,Tab页中创建WebView2控件,使用LoadWebBrowser()方法加载模型。

第2441行代码,将模型与对应的WebView2控件加入集合中,用于在下面的第2个业务场景中。

LoadWebBrowser()方法实现逻辑如下:

public void LoadWebBrowser(WebView2 webView2Control, string bimFaceFileId)
        {
            Node nodeSelected = advTree1.SelectedNode;
            string[] arrTzIdAndSclc = nodeSelected.Name.Split('|');
            string url = ConfigurationManager.AppSettings["BIMFaceReviewPath"];
            url += "?fileId=" + bimFaceFileId
                 + "&tzName=" + HttpUtility.UrlEncode(tzName) // 解决:图纸名称中包含#会截断url
                 + "&xmid=" + _xmid
                 + "&dtgcID=" + _dtgcId
                 + "&tzxxID=" + arrTzIdAndSclc[0]
                 + "&sclc_com=" + arrTzIdAndSclc[1]
                 + "&sczy_com=" + _sczy_com
                 + "&scyjbID=''"  // 意见表ID,这里取不到,设置一个空值。在新增意见的时候才会产生
                 + "&scjlbID=" + _scjlbID
                 + "&scr_sf=" + _scrsf
                 + "&scyjbh=" + _sclc_com
                 + "&gclb_com=" + _gclb_com
                 + "&tz_sczy_code=" + ((NodeTagObject)advTree1.SelectedNode.Tag).TZ_SCZY_Code
                 + "&drawingType=BIM"
                 + "&drawingType2=BIM"
                 + "&sclc_is_change=" + (arrTzIdAndSclc[1].ToInt32() == _sclc_com ? 0 : 1)
                 + "&bimAnnotationId=''";
            //20210621 add by zcn

            // 向网页注册C#对象,供JS调用
            webView2Control.CoreWebView2.AddHostObjectToScript("customWebView2HostObject", new CustomWebView2HostObject());
          webView2Control.Source = new Uri(url);
        }

其中  webView2Control.CoreWebView2.AddHostObjectToScript("customWebView2HostObject", new CustomWebView2HostObject()); 是向目标网页中注入宿主绑定对象,用于JS调用C#方法。用于在下面的第2个业务场景中。

(2)单击模型节点创建Tab页,页签中使用WebView2组件加载网页,渲染智能审查结果。

实现方式如下:

// 查看智能审查引擎结果
private async void btnQueryAIReviewResult_Click(object sender, EventArgs e)
{
    //格式: project_id + dtgc_id + sclc + 工程类别,如:00004361-962-0-FJ
    string batchId = _xmid + "-" + _dtgcId + "-" + _sclc_com + "-" + _gclb_com;
    string aiResult;
    int flag = WebDAL.GetModelCheckProgress(batchId, out aiResult);
    if (flag == 2)
    {
        // 将结果页面集成到系统客户端进行展示
        tabControl_TZ.SelectedTab = tabPage_BIM;

        SimpleResult<int> sr = WebDAL.QueryAIReviewResultFromDB(_xmid, _dtgcId.ToInt32(), _sclc_com, _sczy_com);

        string urlParas = "&batch_id=" + batchId + "&operate_role=ST_ZJ&operator_id=" + Global.gstrUserID + "&operator_name=" + Global.gstrUserName + "&operate_major_code=" + _sczy_com + "&is_confirm=" + sr.ResultObject;

        #region 打开网页

        string nameForTab = batchId;

        #region  如果图纸已经打开,则直接切换到目标tab,无需再创建

        foreach (TabItem tItem in tabControl_BIMFACE.Tabs)
        {
            if (nameForTab == tItem.Name)
            {
                if (dicTzAndWebBrowsers.ContainsKey(nameForTab))
                {
                    tabControl_BIMFACE.SelectedTab = tItem;

                }
                else
                {
                    MessageBox2.ShowError("查看审查意见失败。集合中不存在 WebView2 对象。");
                }

                return;
            }
        }

        #endregion

        if (tabControl_BIMFACE.Tabs.Count > 15)
        {
            MessageBox2.ShowWarning("系统最多只允许打开15个页签。请关闭暂时不用的页签之后再打开新的图纸。");
            return;
        }

        #region 创建新的Tab页签,加载模型并弹出审查意见框

        WebView2 webView2Control = new WebView2();
        webView2Control.Dock = DockStyle.Fill;
        await webView2Control.EnsureCoreWebView2Async(null);

        TabControlPanel tabPanel = new TabControlPanel();
        tabPanel.Name = nameForTab;

        TabItem tabItem = tabControl_BIMFACE.CreateTab(nameForTab);
        tabItem.Name = nameForTab;
        tabItem.Text = "智能审查结果[" + _dtgcmc + "]";
        tabItem.AttachedControl = tabPanel;

        tabPanel.TabItem = tabItem;
        tabPanel.Dock = DockStyle.Fill;

        tabPanel.Controls.Add(webView2Control);

        tabControl_BIMFACE.Controls.Add(tabPanel);
        tabControl_BIMFACE.SelectedTab = tabItem;

        // 向网页注册C#对象,供JS调用
        webView2Control.CoreWebView2.AddHostObjectToScript("customWebView2HostObject", new CustomWebView2HostObject());
        webView2Control.Source = new Uri(aiResult + urlParas);

        #endregion

        dicTzAndWebBrowsers.Add(nameForTab, webView2Control);// 将图纸与浏览器对象加入集合

        #endregion

        LogUtils.Info("专家端审查模型-查看智能审查结果地址:" + aiResult + urlParas);
    }
    else if (flag == 0 || flag == 1)
    {
        MessageBox2.ShowWarning(aiResult);
    }
    else
    {
        // flag == 3 || flag == 4 或者 flag < 0 
        MessageBox2.ShowError(aiResult);
    }
}

业务场景2

审查专家手动审查模型时,填写完审查意见,点击【保存】按钮后,网页中js调用C#方法,将对应的模型节点的“蓝色加号”图标,修改为“黄色警告”图标,表示该模型有审查意见。

实现逻辑如下:

其中926行是获取注入的自定义宿主绑定对象,927行通过该对象调用C#方法来刷新专家审查意见。CustomWebView2HostObject 类的完整定义如下:

 1 using System;
 2 using System.Runtime.InteropServices;
 3 
 4 using Zjgsgtsc.Sczj;
 5 
 6 namespace Zjgsgtsc.SczjWinFrom
 7 {
 8     /// 
 9     /// 自定义宿主类,用于向网页注册C#对象,供JS调用
10     /// 
11     [ClassInterface(ClassInterfaceType.AutoDual)]
12     [ComVisible(true)]
13     public class CustomWebView2HostObject
14     {
15         /// 
16         /// (该方法供网页js调用)网页中保存审查意见后,刷新WinForm中的审查专家意见,以及设置图纸的节点的图标
17         /// 
18         public string RefreshZJSCYJ(int dtgcID, int tzxxID, int sclc_com, string sc_action, string drawingType, string drawingType2)
19         {
20             /* WebView2 是运行在其他线程中的,所以必须使用跨线程的方式进行调用。
21             *  否则无法在目标窗体中创建对象,且访问控件的属性值并不是当前运行时的属性值。
22             */
23 
24             string name = dtgcID + "|" + sc_action;
25 
26             if (drawingType == "BIM")
27             {
28                 if (drawingType2 == "BIM")
29                 {
30                     name += "|BIM";
31 
32                     if (frmMain.DicXmDtAndBIMForm.ContainsKey(name))
33                     {
34                         var form = frmMain.DicXmDtAndBIMForm[name];
35                         form.BeginInvoke(new Action(() =>
36                         {
37                             form.SetNodeImage(tzxxID + "|" + sclc_com, 1);//设置图纸节点。标记为有审查意见
38 
39                             form.LoadYjxx(); //重新加载审查意见列表
40 
41                         }));
42                     }
43                     else
44                     {
45                         // 正常情况下,不会走到该逻辑中
46                         MessageBox2.ShowError("frmMain.DicXmDtAndBIMForm 集合中未找到 Tab 页签。");
47                     }
48                 }
49                 else
50                 {
51                     // 正常情况下,不会走到该逻辑中
52                     MessageBox2.ShowError("frmMain.DicXmDtAndBIMForm 集合中未找到 Tab 页签。");
53                 }
54             }
55 
56             return string.Empty;
57         }
58     }
59 }

重要提醒:

  • 主窗体中创建了多个Tab页,每个Tab页中包含一个模型与对应的WebView2控件。在某个模型网页中审查,点击保存按钮后需要转到Form窗体中找到对应的模型节点。所以首先找到该模型对应的WebView2组件,如34行代码。
  • 第35行,Form窗体程序运行在主线程(UI线程)中,WebView2 是运行在其他线程中的。form.BeginInvoke() 方法获取 创建控件(WebView2)的基础句柄所在的线程(主线程,UI线程),然后异步执行委托,委托中调用窗体中的业务方法实现审查意见列表的更新与节点图标的更换。
  • 自定义的 CustomWebView2HostObject 类,必须标记 [ClassInterface(ClassInterfaceType.AutoDual)]、[ComVisible(true)] 特性,否则JS无法访问到该类,如代码中11、12行。
重新进入

  回调(包括事件处理程序和完成处理程序)是连续运行的。运行事件处理程序并开始消息循环后,事件处理程序或完成回调不能以重入方式运行。如果WebView2应用程序试图在WebView2事件处理程序中同步创建嵌套的消息循环或模式UI,这种方法会导致尝试重新进入。WebView2不支持这种可重入性,它会无限期地将事件处理程序留在堆栈中。

例如,不支持以下编码方法:

private void Btn_Click(object sender, EventArgs e)
{
   // 点击按钮时,向网页提交消息
   this.webView2Control.ExecuteScriptAsync("window.chrome.webview.postMessage(\"Open Dialog\");");
}

private void CoreWebView2_WebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e)
{
   string msg = e.TryGetWebMessageAsString();
   if (msg == "Open Dialog")
   {
      Form1 form = new Form1(); // 当收到web消息时,创建一个包含新WebView2实例的新窗体。
      form.ShowDialog();        // 这将导致重入问题,并导致模式对话框中新创建的WebView2控件挂起。
   }
}

相反,请安排在完成事件处理程序后执行的相应工作,如以下代码所示:

private void CoreWebView2_WebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e)
{
   string msg = e.TryGetWebMessageAsString();
   if (msg == "Open Dialog")
   {
      // 在当前事件处理程序完成后显示一个模式对话框,以避免在WebView2事件处理程序中运行嵌套的消息循环导致潜在的重入问题
      System.Threading.SynchronizationContext.Current.Post((_) => {
         Form1 form = new Form1();
         form.ShowDialog();
         form.Closed();
      }, null);
   }
}

对于 WinForms 和 WPF 应用,若要获取用于调试的完整调用堆栈,必须为 WebView2 应用启用本机代码调试,如下所示:

  1. 在Visual Studio中打开 WebView2 项目。
  2. 在解决方案资源管理器中,右键单击 WebView2 项目,然后选择 “属性”。
  3. 选择 “调试 ”选项卡,然后选中 “启用本机代码调试 ”复选框,如下所示。

延期

  一些WebView2事件读取在相关事件参数上设置的值,或者在事件处理程序完成后启动一些操作。如果还需要运行异步操作,例如事件处理程序,请对关联事件的事件参数使用GetDeferral()方法。返回的延迟对象确保在请求延迟的complete方法之前,事件处理程序不会被认为是已完成的。

  例如,可以使用 NewWindowRequested 事件提供CoreWebView2对象,以便在事件处理程序完成时作为子窗口进行连接。但是,如果需要异步创建CoreWebView2,则应该在 NewWindowRequestedEventArgs 上调用 GetDeleral() 方法。异步创建 CoreWebView2对象 并在 NewWindowRequestedEventArgs上设置 NewWindow 属性后,对 GetDeferral() 方法返回的延迟对象调用Complete方法()。

  • C#语言中的延迟

  在 C# 中使用 Deferral 时,最佳做法是将其与using块一起使用。 即使在using块中间引发异常,该using块也可确保Deferral已完成。 相反,如果显式调用Complete()的代码,但在完成调用之前引发了异常,那么延迟直到一段时间后才完成,此时垃圾收集器最终会收集并处理延迟。在此期间,WebView2会等待应用程序代码处理事件。

  例如,不要执行以下操作,因为如果在调用 Complete之前出现异常, WebResourceRequested 则事件不会被视为“已处理”,并阻止 WebView2 呈现该 Web 内容。

private async void WebView2WebResourceRequestedHandler(CoreWebView2 sender,CoreWebView2WebResourceRequestedEventArgs eventArgs)
{
   var deferral = eventArgs.GetDeferral();
   args.Response = await CreateResponse(eventArgs);
 // 不建议调用Complete,因为如果CreateResponse引发异常,则延迟不会完成。
   deferral.Complete();
}

请改用块 using ,如以下示例所示。 无论是否存在异常,该 using 块都可确保 Deferral 已完成。

private async void WebView2WebResourceRequestedHandler(CoreWebView2 sender,
                           CoreWebView2WebResourceRequestedEventArgs eventArgs)
{// using块确保延迟完成,而不管是否存在异常。
   using (eventArgs.GetDeferral())
   {
      args.Response = await CreateResponse(eventArgs);
   }
}
延期阻止UI线程

  WebView2 依赖于 UI 线程的消息泵来运行事件处理程序回调和异步方法完成回调。 如果使用阻止消息泵的方法(例如 Task.Result 或 WaitForSingleObject),则 WebView2 事件处理程序和异步方法完成处理程序不会运行。 例如,以下代码未完成,因为 Task.Result 在等待 ExecuteScriptAsync 完成时停止消息泵。 由于消息泵被阻止, ExecuteScriptAsync 因此无法完成。

例如,以下代码不起作用,因为它使用 Task.Result

private void Button_Click(object sender, EventArgs e)
{
    string result = webView2Control.CoreWebView2.ExecuteScriptAsync("'test'").Result;
    MessageBox.Show(this, result, "Script Result");
}

相反,请使用异步await机制,例如async、await,不会阻止消息泵或 UI 线程。 例如:

private async void Button_Click(object sender, EventArgs e)
{
    string result = await webView2Control.CoreWebView2.ExecuteScriptAsync("'test'");
    MessageBox.Show(this, result, "Script Result");
}

审图系统业务中创建WebView2控件并初始化CoreWebView2属性以及执行JS脚本时都是使用异步方式

系列目录