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 /// <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脚本时都是使用异步方式

.NET桌面程序应用WebView2组件集成网页开发4 WebView2的线程模型的更多相关文章

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

    系列目录     [已更新最新开发文章,点击查看详细] WebView2 运行时使用与 Microsoft Edge 浏览器相同的进程模型. WebView2 运行时中的进程 WebView2 进程组 ...

  2. 组件化网页开发 / 步骤一 · 4-4 匹配HTML标签

    组件化网页开发 / 步骤一 · 4-4 匹配HTML标签

  3. 组件化网页开发 / 步骤二 · 2-11 jquery的ajax方法 以及下一章跨域没懂

    1,根据2-11课程老师的提示,自己封装 $.ajax 2,第三章,跨域,没看懂

  4. .NET桌面程序集成Web网页开发的多种解决方案

    系列目录     [已更新最新开发文章,点击查看详细] B/S架构的Web程序几乎占据了应用软件的绝大多数市场,但是C/S架构的WinForm.WPF客户端程序依然具有很实用的价值,如设计类软件 Au ...

  5. 网页程序 vs 桌面程序

    网页程序 vs 桌面程序 阅读:  评论:  作者:Rybby  日期:  来源:rybby.com 所谓的网页程序就是指以网页作为程序的操作界面,通过脚本语言“javascript”或其它客户端语言 ...

  6. JRE“瘦身”&桌面程序集成JRE

    项目是一个桌面程序,程序文件不大,但运行jre有198 MB,因此需要"瘦身". jre包含bin.lib两部分,分别为93.6 MB.104 MB. 1.精简bin 运行桌面程序 ...

  7. 桌面程序的其他实现方式----使用WPF窗体展现网页

    需求 在WPF应用程序中,需要使用到WEB项目的资源,第一时间想到的就是在WPF窗口中,展现WEB项目中的页面,这样子有两点好处:一是,实现简单,不需要在WPF应用程序中实现UI布局和数据绑定:二是, ...

  8. DELPHI编写服务程序总结(在系统服务和桌面程序之间共享内存,在服务中使用COM组件)

    DELPHI编写服务程序总结 一.服务程序和桌面程序的区别 Windows 2000/XP/2003等支持一种叫做“系统服务程序”的进程,系统服务和桌面程序的区别是:系统服务不用登陆系统即可运行:系统 ...

  9. 放弃 Electron,拥抱 WebView2!JavaScript 快速开发独立 EXE 程序

    Electron 不错,但也不是完美的. Electron 带来了很多优秀的桌面软件,但并不一定总是适合我们的需求. 多个选择总是好事! 我使用 Electron 遇到的一些麻烦 1.Electron ...

随机推荐

  1. Kafka01--Kafka生产者使用方式

    Kafka之--生产者入门 前言: Kafka诞生至今,产生两个版本的生产者客户端:1是早期基于scala语言编写的客户端:2是随着Java用户的广泛涌入,kafka0.9版本开始退出Java版本的客 ...

  2. springMVC的执行流程?

    springMVC是由dispatchservlet为核心的分层控制框架.首先客户端发出一个请求web服务器解析请求url并去匹配dispatchservlet的映射url,如果匹配上就将这个请求放入 ...

  3. 什么是 Hystrix 断路器?我们需要它吗?

    由于某些原因,employee-consumer 公开服务会引发异常.在这种情况下使用Hystrix 我们定义了一个回退方法.如果在公开服务中发生异常,则回退方法返回一些默认值. 如果 firstPag ...

  4. 学习saltstack (四)

    一.salt常用命令 salt 该命令执行salt的执行模块,通常在master端运行,也是我们最常用到的命令 salt [options] '<target>' <function ...

  5. mysql8.0时区问题

    今天在mysql新增一条数据的时候,发现时间类型的字段比起现在少了8个小时,查了资料才发现,这个是MySQL8.0出现的问题,讲下解决方法. 1.在java项目中application.yml文件中的 ...

  6. sublime text3 好用的插件

    sublime text3 推荐插件 Package Controller安装 1.打开sublime text 3,按ctrl+~或者菜单View > Show Console打开命令窗口.2 ...

  7. HTML5离线存储整理

    前端html部分 //canvas.html <!DOCTYPE html> <html manifest="/test.appcache"> <he ...

  8. java连接oracle数据库(转)

    在做导游通项目所用到 package org.javawo.test; import java.sql.Connection; import java.sql.DriverManager; /** * ...

  9. vue2.0开发聊天程序(八) 初步完成

    项目地址 服务器源码地址:https://github.com/ermu592275254/chat-socket 网页源码地址:https://github.com/ermu592275254/ch ...

  10. HTML 初学整理

    一.HTML简介 HTML的概念 HTML是HyperText Markup Language(超文本标记语言)的简写,超文本标记语言,标准通用标记语言下的一个应用."超文本"就是 ...