.NET桌面程式應用WebView2元件整合網頁開發4 WebView2的執行緒模型

張傳寧發表於2022-04-28

  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控制元件載入網頁,渲染對應的模型。

.NET桌面程式應用WebView2元件整合網頁開發4 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     /// <summary>
 9     /// 自定義宿主類,用於向網頁註冊C#物件,供JS呼叫
10     /// </summary>
11     [ClassInterface(ClassInterfaceType.AutoDual)]
12     [ComVisible(true)]
13     public class CustomWebView2HostObject
14     {
15         /// <summary>
16         /// (該方法供網頁js呼叫)網頁中儲存審查意見後,重新整理WinForm中的審查專家意見,以及設定圖紙的節點的圖示
17         /// </summary>
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指令碼時都是使用非同步方式

相關文章