WebView2控件应用详解系列博客
WebView2控件基于组件对象模型(COM),必须在单线程单元(STA)线程上运行。
下面以真实项目案例(建筑工程施工图BIM人工智能审查系统)讲解WbView2控件如何实现与网页、宿主程序之间进行线程安全的互相通讯。
业务场景1
项目的某个单体下有建筑、结构、给排水、电器、暖通 5个专业,【图纸信息】模型树中上传了4个模型,底部工具栏中有“查看智能审查结果”按钮。
(1)双击模型节点创建Tab页签,页签中使用WebView2控件加载网页,渲染对应的模型。
实现方式如下:
首先判断模型是否已经在Tab页中打开并加载,如果已经加载,则直接切换到对应的Tab页。如果未打开则创建新的Tab页,Tab页中创建WebView2控件,使用LoadWebBrowser()方法加载模型。
第2441行代码,将模型与对应的WebView2控件加入集合中,用于在下面的第2个业务场景中。
LoadWebBrowser()方法实现逻辑如下:
publicvoid 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组件加载网页,渲染智能审查结果。
实现方式如下:
// 查看智能审查引擎结果privateasyncvoid btnQueryAIReviewResult_Click(object sender, EventArgs e) {//格式: project_id + dtgc_id + sclc + 工程类别,如:00004361-962-0-FJstring 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 tItemin tabControl_BIMFACE.Tabs) {if (nameForTab == tItem.Name) {if (dicTzAndWebBrowsers.ContainsKey(nameForTab)) { tabControl_BIMFACE.SelectedTab= tItem; }else { MessageBox2.ShowError("查看审查意见失败。集合中不存在 WebView2 对象。"); }return; } }#endregionif (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); }elseif (flag ==0 || flag ==1) { MessageBox2.ShowWarning(aiResult); }else {// flag == 3 || flag == 4 或者 flag < 0 MessageBox2.ShowError(aiResult); } }
业务场景2
审查专家手动审查模型时,填写完审查意见,点击【保存】按钮后,网页中js调用C#方法,将对应的模型节点的“蓝色加号”图标,修改为“黄色警告”图标,表示该模型有审查意见。
实现逻辑如下:
其中926行是获取注入的自定义宿主绑定对象,927行通过该对象调用C#方法来刷新专家审查意见。CustomWebView2HostObject 类的完整定义如下:
1using System; 2using System.Runtime.InteropServices; 3 4using Zjgsgtsc.Sczj; 5 6namespace Zjgsgtsc.SczjWinFrom 7{ 8///<summary> 9/// 自定义宿主类,用于向网页注册C#对象,供JS调用10///</summary>11 [ClassInterface(ClassInterfaceType.AutoDual)]12 [ComVisible(true)]13publicclass CustomWebView2HostObject14 {15///<summary>16/// (该方法供网页js调用)网页中保存审查意见后,刷新WinForm中的审查专家意见,以及设置图纸的节点的图标17///</summary>18publicstring RefreshZJSCYJ(int dtgcID,int tzxxID,int sclc_com,string sc_action,string drawingType,string drawingType2)19 {20/* WebView2 是运行在其他线程中的,所以必须使用跨线程的方式进行调用。21 * 否则无法在目标窗体中创建对象,且访问控件的属性值并不是当前运行时的属性值。22*/2324string name = dtgcID +"|" + sc_action;2526if (drawingType =="BIM")27 {28if (drawingType2 =="BIM")29 {30 name +="|BIM";3132if (frmMain.DicXmDtAndBIMForm.ContainsKey(name))33 {34var form = frmMain.DicXmDtAndBIMForm[name];35 form.BeginInvoke(new Action(() =>36 {37 form.SetNodeImage(tzxxID +"|" + sclc_com,1);//设置图纸节点。标记为有审查意见3839 form.LoadYjxx();//重新加载审查意见列表4041 }));42 }43else44 {45// 正常情况下,不会走到该逻辑中46 MessageBox2.ShowError("frmMain.DicXmDtAndBIMForm 集合中未找到 Tab 页签。");47 }48 }49else50 {51// 正常情况下,不会走到该逻辑中52 MessageBox2.ShowError("frmMain.DicXmDtAndBIMForm 集合中未找到 Tab 页签。");53 }54 }5556returnstring.Empty;57 }58 }59 }
重要提醒:
回调(包括事件处理程序和完成处理程序)是连续运行的。运行事件处理程序并开始消息循环后,事件处理程序或完成回调不能以重入方式运行。如果WebView2应用程序试图在WebView2事件处理程序中同步创建嵌套的消息循环或模式UI,这种方法会导致尝试重新进入。WebView2不支持这种可重入性,它会无限期地将事件处理程序留在堆栈中。
例如,不支持以下编码方法:
privatevoid Btn_Click(object sender, EventArgs e) {// 点击按钮时,向网页提交消息this.webView2Control.ExecuteScriptAsync("window.chrome.webview.postMessage(\"Open Dialog\");"); }privatevoid CoreWebView2_WebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e) {string msg = e.TryGetWebMessageAsString();if (msg =="Open Dialog") { Form1 form=new Form1();// 当收到web消息时,创建一个包含新WebView2实例的新窗体。 form.ShowDialog();// 这将导致重入问题,并导致模式对话框中新创建的WebView2控件挂起。 } }
相反,请安排在完成事件处理程序后执行的相应工作,如以下代码所示:
privatevoid 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 应用启用本机代码调试,如下所示:
- 在Visual Studio中打开 WebView2 项目。
- 在解决方案资源管理器中,右键单击 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 内容。
privateasyncvoid WebView2WebResourceRequestedHandler(CoreWebView2 sender,CoreWebView2WebResourceRequestedEventArgs eventArgs) {var deferral = eventArgs.GetDeferral(); args.Response=await CreateResponse(eventArgs);// 不建议调用Complete,因为如果CreateResponse引发异常,则延迟不会完成。 deferral.Complete(); }
请改用块using
,如以下示例所示。 无论是否存在异常,该using
块都可确保Deferral
已完成。
privateasyncvoid WebView2WebResourceRequestedHandler(CoreWebView2 sender, CoreWebView2WebResourceRequestedEventArgs eventArgs) {// using块确保延迟完成,而不管是否存在异常。using (eventArgs.GetDeferral()) { args.Response=await CreateResponse(eventArgs); } }
WebView2 依赖于 UI 线程的消息泵来运行事件处理程序回调和异步方法完成回调。 如果使用阻止消息泵的方法(例如Task.Result
或WaitForSingleObject
),则 WebView2 事件处理程序和异步方法完成处理程序不会运行。 例如,以下代码未完成,因为Task.Result
在等待ExecuteScriptAsync
完成时停止消息泵。 由于消息泵被阻止,ExecuteScriptAsync
因此无法完成。
例如,以下代码不起作用,因为它使用Task.Result
。
privatevoid Button_Click(object sender, EventArgs e) {string result = webView2Control.CoreWebView2.ExecuteScriptAsync("'test'").Result; MessageBox.Show(this, result,"Script Result"); }
相反,请使用异步await
机制,例如async、await
,不会阻止消息泵或 UI 线程。 例如:
privateasyncvoid Button_Click(object sender, EventArgs e) {string result =await webView2Control.CoreWebView2.ExecuteScriptAsync("'test'"); MessageBox.Show(this, result,"Script Result"); }
审图系统业务中创建WebView2控件并初始化CoreWebView2属性以及执行JS脚本时都是使用异步方式