开发数据看板的几种方式
1.走 ASP.NET Core 中间层:在 WPF 进程内嵌一个 ASP.NET Core 主机,看板页面请求这个本地服务,由本地服务再去调用 WMS/EMS 的 API。这样浏览器层面始终是同源(都是 http://localhost:某端口),跨域问题在服务端用 HttpClient 解决,不受浏览器限制。模式一:ASP.NET Core + WebView2 集成
2.用 WebView2 的宿主对象通信:通过 WebView2 的 AddHostObjectToScript 或 WebMessage 机制,让 HTML 页面把请求交给 WPF 的 C# 代码,由 C# 用 HttpClient 去调用外部系统。这样完全绕开浏览器跨域,但通信方式不再是纯 HTTP 请求,而是走宿主桥接。
示例:这套完整案例,核心通信机制就是 AddHostObjectToScript,同时也混用了 WebMessage 通道作为补充,实现双向通讯。
<Window x:Class="WebView2DashboardDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WebView2DashboardDemo" mc:Ignorable="d" xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf" Title="产线实时监控看板" Height="900" Width="1600" Background="#0a111f" WindowStartupLocation="CenterScreen"> <Grid> <wv2:WebView2 x:Name="webView" /> <Border x:Name="loadingMask" Background="#0a111f"> <TextBlock Text="看板加载中..." Foreground="#22d3c5" FontSize="18" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Border> </Grid> </Window>
/// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { private DashboardBridge _bridge; private bool _pageReady = false; private string _pendingPush = null; public MainWindow() { InitializeComponent(); InitializeWebView(); } private async void InitializeWebView() { await webView.EnsureCoreWebView2Async(); // 1. 允许 file:// 页面访问宿主对象 webView.CoreWebView2.Settings.IsWebMessageEnabled = true; webView.CoreWebView2.Settings.AreHostObjectsAllowed = true; // 2. 创建桥接对象并注册到 JS _bridge = new DashboardBridge { PushToJs = payload => { // 页面没准备好时缓存,等 NavigationCompleted 后补发 if (!_pageReady) { _pendingPush = payload; return; } // 用 PostWebMessageAsJson 推送,比 ExecuteScriptAsync 更安全 webView.CoreWebView2.PostWebMessageAsJson(payload); } }; webView.CoreWebView2.AddHostObjectToScript("bridge", _bridge); // 3. 订阅 JS 发来的消息(postMessage 通道) webView.CoreWebView2.WebMessageReceived += OnWebMessageReceived; // 4. 页面加载完成 webView.NavigationCompleted += OnNavigationCompleted; // 5. 加载本地 HTML string htmlPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"Assets\dashboard.html"); webView.Source = new Uri(htmlPath); } private async void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e) { if (!e.IsSuccess) return; _pageReady = true; loadingMask.Visibility = Visibility.Collapsed; // 补发缓存的数据 if (_pendingPush != null) { webView.CoreWebView2.PostWebMessageAsJson(_pendingPush); _pendingPush = null; } // 推一次初始快照 string init = _bridge.GetRealtimeData(); webView.CoreWebView2.PostWebMessageAsJson(init); await System.Threading.Tasks.Task.CompletedTask; } private void OnWebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e) { // JS 通过 window.chrome.webview.postMessage(...) 发来的消息 string json = e.WebMessageAsJson; // 这里可以按业务解析,比如 JS 通知 C# 记录日志、写库等 System.Diagnostics.Debug.WriteLine($"[JS→C#] {json}"); } }
/// <summary> /// 暴露给 JavaScript 的桥接对象 /// JS 通过 window.chrome.webview.hostObjects.bridge 调用这里的方法 /// </summary> /// [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] public class DashboardBridge { private readonly Random _rand = new Random(); private readonly DispatcherTimer _timer; private string _currentLine = "A"; /// <summary> /// C# 主动向 JS 推送数据的委托(由 MainWindow 注入) /// </summary> public Action<string> PushToJs { get; set; } public DashboardBridge() { // 每 2 秒主动推送一次模拟数据 _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; _timer.Tick += (s, e) => PushToJs?.Invoke(BuildPayload()); _timer.Start(); } /// <summary> /// JS 主动调用:获取当前产线快照 /// </summary> public string GetRealtimeData() { return BuildPayload(); } /// <summary> /// JS 主动调用:切换产线,返回切换后的快照 /// </summary> public string SwitchLine(string line) { _currentLine = line; return BuildPayload(); } /// <summary> /// JS 主动调用:获取历史趋势(示例) /// </summary> public string GetTrend(int points) { var list = new List<object>(); for (int i = 0; i < points; i++) { list.Add(new { time = DateTime.Now.AddSeconds(-2 * (points - i)).ToString("HH:mm:ss"), value = _rand.Next(800, 1500) }); } return JsonSerializer.Serialize(list); } /// <summary> /// JS 主动调用:确认告警(示例业务动作) /// </summary> public string AcknowledgeAlarm(int alarmId) { // 真实场景:写数据库 / 调 MES 接口 return JsonSerializer.Serialize(new { success = true, alarmId, time = DateTime.Now }); } private string BuildPayload() { // 真实场景:调 WMS 接口拿库存,调 MES 拿工单 //var data = _httpClient.GetStringAsync("http://wms-server/api/line/A").Result; //return data; var payload = new { line = _currentLine, output = _rand.Next(800, 1500), yield = Math.Round(_rand.NextDouble() * 5 + 95, 2), alarm = _rand.Next(0, 6), oee = Math.Round(_rand.NextDouble() * 10 + 82, 2), stations = new[] { _rand.Next(250, 450), _rand.Next(250, 450), _rand.Next(250, 450), _rand.Next(250, 450) }, bad = _rand.Next(5, 30), pending = _rand.Next(3, 15), devices = new[] { _rand.Next(0, 2) == 1, _rand.Next(0, 2) == 1, _rand.Next(0, 2) == 1 }, alarms = new[] { new { msg = "上料机械手扭矩波动", level = "warn", time = DateTime.Now.AddMinutes(-1).ToString("HH:mm:ss") }, new { msg = "2号工位视觉定位偏差", level = "error", time = DateTime.Now.AddMinutes(-3).ToString("HH:mm:ss") } } }; return JsonSerializer.Serialize(payload); } }
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>产线实时监控看板</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5.6.0/dist/echarts.min.js"></script> <style> :root{ --bg:#0a111f; --panel:#111b2d; --panel-2:#0e1728; --line:#1e2d47; --text:#e8f0fc; --dim:#8aa0c0; --cyan:#22d3c5; --blue:#5aa0ff; --green:#34d399; --red:#f87171; --amber:#fbbf24; } *{margin:0;padding:0;box-sizing:border-box} html,body{height:100%} body{font-family:"PingFang SC","Microsoft YaHei",system-ui,sans-serif; background:var(--bg);color:var(--text);padding:16px;min-height:100vh} .topbar{display:flex;align-items:center;justify-content:space-between; padding:10px 18px;margin-bottom:14px;background:var(--panel); border:1px solid var(--line);position:relative} .topbar::before,.topbar::after{content:"";position:absolute;width:14px;height:14px;border:1px solid var(--cyan)} .topbar::before{top:-1px;left:-1px;border-right:none;border-bottom:none} .topbar::after{bottom:-1px;right:-1px;border-left:none;border-top:none} .title h1{font-size:22px;font-weight:700;letter-spacing:2px} .title p{font-size:11px;color:var(--dim);margin-top:3px;letter-spacing:1px} .tabs{display:flex;gap:8px} .tab{padding:7px 18px;cursor:pointer;font-size:13px;color:var(--dim); border:1px solid var(--line);background:transparent;transition:all .2s} .tab:hover{color:var(--text);border-color:var(--cyan)} .tab.active{color:#062a26;background:var(--cyan);border-color:var(--cyan);font-weight:600} .clock{font-size:14px;color:var(--cyan);font-variant-numeric:tabular-nums;text-align:right} .clock span{display:block;font-size:11px;color:var(--dim);margin-top:2px} .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:14px} .kpi{background:var(--panel);border:1px solid var(--line);padding:14px 16px;position:relative;overflow:hidden} .kpi .label{font-size:12px;color:var(--dim)} .kpi .value{font-size:30px;font-weight:700;margin-top:8px;font-variant-numeric:tabular-nums;line-height:1} .kpi .sub{font-size:11px;color:var(--dim);margin-top:8px} .kpi .tick{position:absolute;top:0;left:0;width:3px;height:100%;background:var(--cyan)} .kpi.alert .tick{background:var(--red)} .kpi.alert .value{color:var(--red)} .grid{display:grid;grid-template-columns:1.9fr 1fr;gap:12px;margin-bottom:12px} .grid-bottom{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:12px} .panel{background:var(--panel);border:1px solid var(--line);padding:14px 16px;position:relative} .panel h2{font-size:14px;font-weight:600;margin-bottom:4px;display:flex;align-items:center;gap:8px} .panel h2::before{content:"";width:3px;height:14px;background:var(--cyan);display:inline-block} .panel .note{font-size:11px;color:var(--dim);margin-bottom:8px} .chart{width:100%;min-height:300px} .alarms{display:flex;flex-direction:column;gap:8px;overflow:hidden} .alarm-row{display:flex;align-items:center;gap:10px;padding:9px 10px; background:var(--panel-2);border-left:3px solid var(--amber);font-size:12px} .alarm-row.error{border-left-color:var(--red)} .alarm-row .time{color:var(--dim);font-variant-numeric:tabular-nums;white-space:nowrap} .alarm-row .msg{color:var(--text);flex:1} .alarm-row .level{font-size:10px;padding:2px 6px;white-space:nowrap} .alarm-row .level.warn{background:rgba(251,191,36,.15);color:var(--amber)} .alarm-row .level.error{background:rgba(248,113,113,.15);color:var(--red)} .alarm-row .ack{cursor:pointer;font-size:10px;color:var(--cyan); border:1px solid var(--cyan);padding:1px 6px;background:transparent} .alarm-row .ack:hover{background:var(--cyan);color:#062a26} .mock-tag{font-size:10px;color:var(--amber);border:1px dashed var(--amber); padding:1px 6px;letter-spacing:1px} #bridgeStatus{font-size:11px;color:var(--dim);margin-left:8px} </style> </head> <body> <div class="topbar"> <div class="title"> <h1>产线实时监控中心</h1> <p>INDUSTRIAL LINE MONITOR <span class="mock-tag">BRIDGE · DEMO</span> <span id="bridgeStatus">桥接状态:检测中…</span></p> </div> <div class="tabs" id="lineTabs"> <button class="tab active" data-line="A">产线 A</button> <button class="tab" data-line="B">产线 B</button> <button class="tab" data-line="C">产线 C</button> </div> <div class="clock"> <span id="clockDate"></span> <span id="clockTime"></span> </div> </div> <div class="kpis"> <div class="kpi"><div class="tick"></div> <div class="label">今日产量</div> <div class="value" id="kpiOutput">--</div> <div class="sub" id="kpiOutputSub">班次目标完成率 --</div> </div> <div class="kpi"><div class="tick"></div> <div class="label">综合良率</div> <div class="value" id="kpiYield">--</div> <div class="sub" id="kpiYieldSub">不良数 --</div> </div> <div class="kpi alert"><div class="tick"></div> <div class="label">在产告警</div> <div class="value" id="kpiAlarm">--</div> <div class="sub" id="kpiAlarmSub">其中严重 --</div> </div> <div class="kpi"><div class="tick"></div> <div class="label">设备 OEE</div> <div class="value" id="kpiOee">--</div> <div class="sub">稼动率 --</div> </div> </div> <div class="grid"> <div class="panel"> <h2>实时产量趋势</h2> <div class="note">最近 30 个采样点 · 每 2 秒刷新</div> <div id="trendChart" class="chart"></div> </div> <div class="panel"> <h2>实时告警列表 <button id="btnAckAll" style="float:right;font-size:11px;color:var(--cyan); border:1px solid var(--cyan);background:transparent;padding:2px 8px;cursor:pointer">全部确认</button></h2> <div class="note" id="alarmNote">点击“确认”通过桥接调用 C#</div> <div class="alarms" id="alarmList"></div> </div> </div> <div class="grid-bottom"> <div class="panel"><h2>工位产量对比</h2><div class="note">各工位当日产量</div> <div id="barChart" class="chart"></div></div> <div class="panel"><h2>质量构成</h2><div class="note">良品 / 不良 / 待检</div> <div id="pieChart" class="chart"></div></div> <div class="panel"><h2>设备状态</h2><div class="note">关键设备运行状态</div> <div id="devicePanel"></div></div> </div> <script> /* ==================== 桥接对象获取 ==================== */ let bridge = null; function initBridge() { return new Promise((resolve) => { const tryGet = () => { if (window.chrome?.webview?.hostObjects?.bridge) { bridge = window.chrome.webview.hostObjects.bridge; document.getElementById('bridgeStatus').textContent = '桥接状态:已连接 ✅'; resolve(bridge); } else { setTimeout(tryGet, 100); } }; tryGet(); }); } /* ==================== 图表初始化 ==================== */ let trendChart, barChart, pieChart; const MAX_POINTS = 30; let trendData = []; function initCharts() { trendChart = echarts.init(document.getElementById('trendChart')); trendChart.setOption({ grid:{left:45,right:20,top:20,bottom:30}, tooltip:{trigger:'axis',backgroundColor:'#111b2d',borderColor:'#1e2d47',textStyle:{color:'#e8f0fc'}}, xAxis:{type:'category',boundaryGap:false,data:[],axisLine:{lineStyle:{color:'#1e2d47'}},axisLabel:{color:'#8aa0c0',fontSize:10}}, yAxis:{type:'value',splitLine:{lineStyle:{color:'#1e2d47',type:'dashed'}},axisLabel:{color:'#8aa0c0',fontSize:10}}, series:[{type:'line',smooth:true,symbol:'circle',symbolSize:5,data:[], lineStyle:{width:2,color:'#22d3c5'},itemStyle:{color:'#22d3c5'}, areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[ {offset:0,color:'rgba(34,211,197,0.35)'},{offset:1,color:'rgba(34,211,197,0.02)'}])}}] }); barChart = echarts.init(document.getElementById('barChart')); barChart.setOption({ grid:{left:40,right:15,top:20,bottom:30}, tooltip:{trigger:'axis',backgroundColor:'#111b2d',borderColor:'#1e2d47',textStyle:{color:'#e8f0fc'}}, xAxis:{type:'category',data:['工位1','工位2','工位3','工位4'],axisLine:{lineStyle:{color:'#1e2d47'}},axisLabel:{color:'#8aa0c0',fontSize:10}}, yAxis:{type:'value',splitLine:{lineStyle:{color:'#1e2d47',type:'dashed'}},axisLabel:{color:'#8aa0c0',fontSize:10}}, series:[{type:'bar',data:[],barWidth:'45%', itemStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[ {offset:0,color:'#5aa0ff'},{offset:1,color:'#22d3c5'}]),borderRadius:[3,3,0,0]}, label:{show:true,position:'top',color:'#8aa0c0',fontSize:10}}] }); pieChart = echarts.init(document.getElementById('pieChart')); pieChart.setOption({ tooltip:{trigger:'item',backgroundColor:'#111b2d',borderColor:'#1e2d47',textStyle:{color:'#e8f0fc'}}, legend:{bottom:0,textStyle:{color:'#8aa0c0',fontSize:11}}, series:[{type:'pie',radius:['48%','70%'],center:['50%','45%'], itemStyle:{borderColor:'#111b2d',borderWidth:2}, label:{color:'#e8f0fc',fontSize:11,formatter:'{b}\n{d}%'}, labelLine:{lineStyle:{color:'#1e2d47'}}, data:[{value:0,name:'良品',itemStyle:{color:'#34d399'}}, {value:0,name:'不良',itemStyle:{color:'#f87171'}}, {value:0,name:'待检',itemStyle:{color:'#fbbf24'}}]}] }); window.addEventListener('resize', () => { trendChart.resize(); barChart.resize(); pieChart.resize(); }); } /* ==================== 时钟 ==================== */ function updateClock() { const now = new Date(); const week = ['日','一','二','三','四','五','六'][now.getDay()]; document.getElementById('clockDate').textContent = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} 周${week}`; document.getElementById('clockTime').textContent = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`; } setInterval(updateClock, 1000); /* ==================== 渲染函数 ==================== */ function renderAll(d) { document.getElementById('kpiOutput').textContent = d.output ?? '--'; document.getElementById('kpiOutputSub').textContent = `班次目标完成率 ${d.output ? ((d.output/1500)*100).toFixed(1) : '--'}%`; document.getElementById('kpiYield').textContent = d.yield != null ? d.yield + '%' : '--'; document.getElementById('kpiYieldSub').textContent = `不良数 ${d.bad ?? '--'}`; document.getElementById('kpiAlarm').textContent = d.alarm ?? '--'; const severe = (d.alarms||[]).filter(a=>a.level==='error').length; document.getElementById('kpiAlarmSub').textContent = `其中严重 ${severe}`; document.getElementById('kpiOee').textContent = d.oee != null ? d.oee + '%' : '--'; if (d.output != null) { const now = new Date(); trendData.push({ label:`${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`, value:d.output }); if (trendData.length > MAX_POINTS) trendData.shift(); trendChart.setOption({ xAxis:{data:trendData.map(p=>p.label)}, series:[{data:trendData.map(p=>p.value)}] }); } if (d.stations) barChart.setOption({ series:[{data:d.stations}] }); if (d.output != null) { const good = Math.max(d.output - (d.bad||0) - (d.pending||0), 0); pieChart.setOption({ series:[{data:[ {value:good,name:'良品',itemStyle:{color:'#34d399'}}, {value:d.bad||0,name:'不良',itemStyle:{color:'#f87171'}}, {value:d.pending||0,name:'待检',itemStyle:{color:'#fbbf24'}}]}]}); } if (d.devices) { const names = ['贴片机','回流焊','AOI']; document.getElementById('devicePanel').innerHTML = d.devices.map((ok,i)=>` <div style="display:flex;align-items:center;gap:10px;padding:10px;margin-bottom:8px; background:var(--panel-2);border-left:3px solid ${ok?'var(--green)':'var(--red)'}"> <span style="width:8px;height:8px;border-radius:50%;background:${ok?'var(--green)':'var(--red)'}; box-shadow:0 0 8px ${ok?'var(--green)':'var(--red)'}"></span> <span style="flex:1;font-size:13px">${names[i]||'设备'+(i+1)}</span> <span style="font-size:12px;color:${ok?'var(--green)':'var(--red)'}">${ok?'运行中':'停机'}</span> </div>`).join(''); } if (d.alarms) renderAlarms(d.alarms); if (d.line) document.getElementById('alarmNote').textContent = `产线 ${d.line} · 点击“确认”调用 C#`; } function renderAlarms(alarms) { const box = document.getElementById('alarmList'); if (!alarms || alarms.length === 0) { box.innerHTML = `<div style="color:var(--dim);font-size:12px;padding:12px 4px">当前无告警</div>`; return; } box.innerHTML = alarms.map((a,i)=>` <div class="alarm-row ${a.level==='error'?'error':''}"> <span class="time">${a.time||'--:--:--'}</span> <span class="msg">${a.msg||''}</span> <span class="level ${a.level==='error'?'error':'warn'}">${a.level==='error'?'严重':'警告'}</span> <button class="ack" data-idx="${i}">确认</button> </div>`).join(''); box.querySelectorAll('.ack').forEach(btn=>{ btn.onclick = async () => { if (!bridge) return; // ★ 关键:JS 通过桥接主动调用 C# 方法 const res = await bridge.AcknowledgeAlarm(parseInt(btn.dataset.idx)); btn.textContent = '已确认'; btn.disabled = true; btn.style.opacity = 0.5; console.log('[JS→C#] AcknowledgeAlarm 返回:', res); }; }); } /* ==================== C# 推送数据接收(PostWebMessageAsJson) ==================== */ window.chrome?.webview?.addEventListener('message', (e) => { // e.data 就是 C# PostWebMessageAsJson 推送的 JSON 对象 renderAll(e.data); }); /* ==================== 产线切换:通过桥接调用 C# ==================== */ document.getElementById('lineTabs').addEventListener('click', async (e) => { const btn = e.target.closest('.tab'); if (!btn) return; document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active')); btn.classList.add('active'); const line = btn.dataset.line; trendData = []; trendChart.setOption({ xAxis:{data:[]}, series:[{data:[]}] }); if (bridge) { // ★ JS 主动调用 C# 的 SwitchLine,返回该产线快照 const json = await bridge.SwitchLine(line); renderAll(JSON.parse(json)); // 同时通过 postMessage 通知 C#(走 WebMessageReceived 通道) window.chrome.webview.postMessage({ type:'switchLine', line }); } }); /* ==================== 全部确认 ==================== */ document.getElementById('btnAckAll').onclick = async () => { if (!bridge) return; const res = await bridge.AcknowledgeAlarm(-1); console.log('全部确认返回:', res); document.querySelectorAll('.ack').forEach(b=>{ b.textContent='已确认'; b.disabled=true; }); }; /* ==================== 启动 ==================== */ (async function boot() { initCharts(); updateClock(); await initBridge(); // 等待桥接就绪 if (bridge) { // 首屏主动拉一次数据 const json = await bridge.GetRealtimeData(); renderAll(JSON.parse(json)); } })(); </script> </body> </html>
通道一:AddHostObjectToScript(宿主对象桥接)这条通道负责 JS → C# 的“请求-响应”,
//CSharp webView.CoreWebView2.AddHostObjectToScript("bridge", _bridge); //js const bridge = window.chrome.webview.hostObjects.bridge; const json = await bridge.GetRealtimeData(); // JS 主动调 C# const snapshot = await bridge.SwitchLine('B'); // 带参数、有返回值 await bridge.AcknowledgeAlarm(3); // 触发业务动作
通道二:WebMessage(消息通道)负责 C# → JS 的定时推送,以及 JS → C# 的轻量通知。
//csharp webView.CoreWebView2.PostWebMessageAsJson(payload); // C# → JS webView.CoreWebView2.WebMessageReceived += OnWebMessageReceived; // JS → C# //js window.chrome.webview.addEventListener('message', e => renderAll(e.data)); // 收推送 window.chrome.webview.postMessage({ type:'switchLine', line }); // 发通知,无需数据返回
解析:
DashboardBridge 类是C#和JavaScript之间专门建立的一个通信中转对象,他让HTML页面中的JS代码能够像调用本地函数一样直接调用C#方法。
DashboardBridge 被注册到 WebView2 后,JS 侧就能通过一个固定的入口访问它:
//javascript中的固定入口 const bridge = window.chrome.webview.hostObjects.bridge; //bridge 这个名字,就是你调用 AddHostObjectToScript("bridge", _bridge) 时起的 //js中可以执行操作让C#执行 // JS 主动调用 C# 方法,拿返回值 const json = await bridge.GetRealtimeData(); renderAll(JSON.parse(json)); // JS 触发一个业务动作,让 C# 去处理 await bridge.AcknowledgeAlarm(3); // JS 告诉 C# 切换产线 const snapshot = await bridge.SwitchLine('B'); //对应到 C# 侧,就是 DashboardBridge 类里的这几个方法 public string GetRealtimeData() { ... } public string SwitchLine(string line) { ... } public string AcknowledgeAlarm(int alarmId) { ... }
他解决了什么问题?
1. 让前端能“主动拉数据”
看板刚打开、或者用户手动刷新时,JS 不需要等 C# 的定时器,直接 await bridge.GetRealtimeData() 就能拿到最新快照。
2. 让前端能“触发业务动作”
告警列表里每一条后面的“确认”按钮,点击后 JS 调用 bridge.AcknowledgeAlarm(idx),C# 收到后可以去写数据库、调 MES 接口、记日志。这些操作 JS 自己做不了,必须交给 C#。
3. 把外部系统调用封装在 C# 侧
这是最关键的一点。假设你要对接 WMS 或 EMS,真实的数据来自它们的 HTTP API。你可以在 DashboardBridge 里用 HttpClient 去调这些接口:
public string GetRealtimeData() { // 真实场景:调 WMS 接口拿库存,调 MES 拿工单 var data = _httpClient.GetStringAsync("http://wms-server/api/line/A").Result; return data; }
JS 侧完全不用关心 WMS 的地址、认证、跨域问题,它只管 await bridge.GetRealtimeData()。桥接层把“前端展示”和“后端集成”彻底隔离开了。
HTML如何接收到C#的数据呢?
利用PostWebMessageAsJson 推送的数据,在 dashboard.html 里是通过 window.chrome.webview 的 message 事件接收的。对应你 HTML 里这一段:
/* ==================== C# 推送数据接收(PostWebMessageAsJson)addEventListener 要尽早注册,否则 C# 在页面还没注册监听时就推送,消息会丢失 ==================== */
window.chrome?.webview?.addEventListener('message', (e) => {
// e.data 就是 C# PostWebMessageAsJson 推送的 JSON 对象
renderAll(e.data);//e.data是对象不是字符串
});
boot()的作用?
dashboard.html 里,boot() 是整个页面的启动入口函数。它负责在页面加载完成后,按正确的顺序把看板“点燃”——初始化图表、启动时钟、建立桥接、拉取首屏数据
dashboard.html 中主要的交互:
-
addEventListener('message', ...)接收 C# 推送 -
await bridge.SwitchLine(line)调用 C# 方法 -
window.chrome.webview.postMessage({...})通知 C#
PostWebMessageAsJson 和 postMessage 的区别?

window.chrome?.webview?.addEventListener('message', (e) => { const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data; renderAll(data); });js中可以加兼容
区别:
ASP.NET Core 中间层是把“Web 开发”的完整能力搬进了 WPF,适合逻辑复杂、需要复用 Web 生态的场景;而 WebView2 宿主对象则是把“C# 能力”直接嫁接给了 JavaScript,适合轻量、直接、追求极致性能的本地交互。
浙公网安备 33010602011771号