系列目录 【已更新最新开发文章,点击查看详细】
WebView2控件基于组件对象模型(COM),必须在单线程单元(STA)线程上运行。
线程安全
下面以真实项目案例(建筑工程施工图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 /// <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 }
重要提醒:
重新进入
回调(包括事件处理程序和完成处理程序)是连续运行的。运行事件处理程序并开始消息循环后,事件处理程序或完成回调不能以重入方式运行。如果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 应用启用本机代码调试,如下所示:
延期
一些WebView2事件读取在相关事件参数上设置的值,或者在事件处理程序完成后启动一些操作。如果还需要运行异步操作,例如事件处理程序,请对关联事件的事件参数使用GetDeferral()方法。返回的延迟对象确保在请求延迟的complete方法之前,事件处理程序不会被认为是已完成的。
例如,可以使用 NewWindowRequested 事件提供CoreWebView2对象,以便在事件处理程序完成时作为子窗口进行连接。但是,如果需要异步创建CoreWebView2,则应该在 NewWindowRequestedEventArgs 上调用 GetDeleral() 方法。异步创建 CoreWebView2对象 并在 NewWindowRequestedEventArgs上设置 NewWindow 属性后,对 GetDeferral() 方法返回的延迟对象调用Complete方法()。
在 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脚本时都是使用异步方式
系列目录 【已更新最新开发文章,点击查看详细】