WPF和web泛型通信

This commit is contained in:
ShaoHua
2026-03-11 22:25:30 +08:00
parent be9b042a05
commit 295a05d4ae
9 changed files with 506 additions and 70 deletions
+9 -3
View File
@@ -9,6 +9,7 @@ using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Microsoft.Web.WebView2.Core;
using WebView2Demo.WebInterop;
namespace WebView2Demo
{
@@ -17,6 +18,8 @@ namespace WebView2Demo
/// </summary>
public partial class MainWindow : Window
{
private MainSystemGateway _gateway = new();
public MainWindow()
{
InitializeComponent();
@@ -33,11 +36,14 @@ namespace WebView2Demo
// 2. 注入调试开启指令 (可选)
webView.CoreWebView2.Settings.AreDevToolsEnabled = true;
// 3. 监听消息
// 3. 注入网关 Host Object (供前端调用)
webView.CoreWebView2.AddHostObjectToScript("gateway", _gateway);
// 4. 监听消息
webView.WebMessageReceived += WebView_WebMessageReceived;
// 4. 初始化完成后再导航,确保 window.chrome.webview 注入成功
webView.Source = new Uri("http://localhost:5173");
// 5. 初始化完成后再导航
webView.Source = new Uri("http://localhost:5174");
}
catch (Exception ex)
{
+60
View File
@@ -0,0 +1,60 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace WebView2Demo.Models
{
/// <summary>
/// 标准 API 请求信封 (泛型版本,方便内部使用)
/// </summary>
/// <typeparam name="T">具体的业务数据类型</typeparam>
public class StandardApiRequest<T>
{
[JsonPropertyName("action")]
public string Action { get; set; } = string.Empty;
[JsonPropertyName("data")]
public T? Data { get; set; }
}
/// <summary>
/// 基础请求信封 (用于初次解析 Action)
/// </summary>
public class StandardApiRequest : StandardApiRequest<JsonElement> { }
/// <summary>
/// 标准 API 响应信封
/// </summary>
/// <typeparam name="T">返回的业务数据类型</typeparam>
public class StandardApiResponse<T>
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
[JsonPropertyName("data")]
public T? Data { get; set; }
[JsonPropertyName("timestamp")]
public long Timestamp { get; set; } = DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
/// <summary>
/// 基础响应信封 (默认返回对象类型)
/// </summary>
public class StandardApiResponse : StandardApiResponse<object> { }
/// <summary>
/// 示例用的业务参数模型
/// </summary>
public class CalculationParams
{
[JsonPropertyName("a")]
public int A { get; set; }
[JsonPropertyName("b")]
public int B { get; set; }
}
}
+123
View File
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
using WebView2Demo.WebInterop.Attributes;
namespace WebView2Demo.WebInterop
{
/// <summary>
/// API 路由管理器:负责扫描 [WebApiControl] 和 [WebAction] 并执行分发
/// </summary>
public class ApiRouteManager
{
// 缓存映射: ActionName -> (Instance, MethodInfo)
private readonly Dictionary<string, (object Instance, MethodInfo Method)> _routeTable = new();
private readonly JsonSerializerOptions _jsonOptions;
public ApiRouteManager(JsonSerializerOptions jsonOptions)
{
_jsonOptions = jsonOptions;
ScanAndRegisterRoutes();
}
private void ScanAndRegisterRoutes()
{
// 扫描当前程序集所有标记了 [WebApiControl] 的类
var types = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => t.GetCustomAttribute<WebApiControlAttribute>() != null);
foreach (var type in types)
{
// 创建业务类实例 (生产环境建议从 DI 容器获取)
var instance = Activator.CreateInstance(type);
if (instance == null) continue;
// 扫描所有标记了 [WebAction] 的方法
var methods = type.GetMethods()
.Where(m => m.GetCustomAttribute<WebActionAttribute>() != null);
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<WebActionAttribute>();
// 如果没指定 ActionName,则使用方法名
string actionName = (attr?.ActionName ?? method.Name).ToLower();
if (!_routeTable.TryAdd(actionName, (instance, method)))
{
throw new InvalidOperationException($"重复的 API 路由注册: {actionName}");
}
}
}
}
/// <summary>
/// 核心分发逻辑
/// </summary>
public async Task<(object? Data, string Message)> DispatchActionAsync(string action, JsonElement rawData)
{
if (!_routeTable.TryGetValue(action.ToLower(), out var route))
{
return (null, $"未找到对应的业务逻辑: {action}");
}
try
{
var method = route.Method;
var instance = route.Instance;
var parameters = method.GetParameters();
object?[]? invokeArgs = null;
// 自动参数解析 (支持泛型)
if (parameters.Length > 0)
{
var paramType = parameters[0].ParameterType;
// 安全检查:如果 rawData 是空的或未定义的,尝试传递 null 或默认值
if (rawData.ValueKind == JsonValueKind.Undefined || rawData.ValueKind == JsonValueKind.Null)
{
invokeArgs = new object?[] { null };
}
else
{
// 将原始 JsonElement 转换为方法所需的参数类型
var arg = JsonSerializer.Deserialize(rawData.GetRawText(), paramType, _jsonOptions);
invokeArgs = new[] { arg };
}
}
// 执行方法
object? result = method.Invoke(instance, invokeArgs);
// 处理 Task 返回值
if (result is Task task)
{
await task;
// 尝试获取 Task<T> 的结果 (Result 属性)
var resultProperty = task.GetType().GetProperty("Result");
if (resultProperty != null)
{
return (resultProperty.GetValue(task), "操作成功");
}
else
{
// 普通 Task,没有返回值
return (null, "操作成功");
}
}
return (result, "操作成功");
}
catch (TargetInvocationException ex)
{
return (null, $"业务执行异常: {ex.InnerException?.Message ?? ex.Message}");
}
catch (Exception ex)
{
return (null, $"网关解析异常: {ex.Message}");
}
}
}
}
@@ -0,0 +1,20 @@
using System;
namespace WebView2Demo.WebInterop.Attributes
{
/// <summary>
/// 标记业务类为 API 控制器
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class WebApiControlAttribute : Attribute { }
/// <summary>
/// 标记方法为可被前端调用的 Action
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class WebActionAttribute : Attribute
{
public string? ActionName { get; }
public WebActionAttribute(string? actionName = null) => ActionName = actionName;
}
}
@@ -0,0 +1,101 @@
using System;
using System.Runtime.InteropServices;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;
using WebView2Demo.Models;
namespace WebView2Demo.WebInterop
{
/// <summary>
/// 前后端互操作主网关类 [反射分发, 统一回包]
/// </summary>
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
public class MainSystemGateway
{
private readonly ApiRouteManager _routeManager;
private readonly JsonSerializerOptions _jsonOptions = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNameCaseInsensitive = true,
WriteIndented = false
};
public MainSystemGateway()
{
// 初始化路由管理器
_routeManager = new ApiRouteManager(_jsonOptions);
}
/// <summary>
/// 统一 API 调用入口 (前端唯一调用的方法)
/// 遵循 COM 规范,接收 string 并返回 string
/// </summary>
public async Task<string> InvokeAction(string requestJson)
{
// 这里我们调用内部泛型处理逻辑,T 使用 JsonElement 作为通用基类
return await ProcessRequestInternal<JsonElement>(requestJson);
}
/// <summary>
/// 内部泛型处理逻辑:在这里进行真正的协议解析和分发
/// </summary>
/// <typeparam name="T">请求数据的泛型类型</typeparam>
private async Task<string> ProcessRequestInternal<T>(string requestJson)
{
try
{
// 1. 解析请求信封 (进入泛型世界)
var request = JsonSerializer.Deserialize<StandardApiRequest<T>>(requestJson, _jsonOptions);
if (request == null || string.IsNullOrWhiteSpace(request.Action))
{
return MakeErrorResponse("请求 JSON 格式错误或 Action 缺失");
}
// 2. 路由分发 (通过反射查找 Service 和 Method 并执行)
// 注意:由于 ApiRouteManager 内部处理了 JsonElement 的动态解析,这里我们传入 Data 即可
JsonElement rawData = request.Data is JsonElement element ? element : default;
var (data, msg) = await _routeManager.DispatchActionAsync(request.Action, rawData);
// 3. 根据结果统一回包
if (data == null && msg.Contains("未找到"))
{
return MakeErrorResponse(msg);
}
return MakeSuccessResponse(data!, msg);
}
catch (Exception ex)
{
return MakeErrorResponse($"网关内部崩溃: {ex.Message}");
}
}
#region [ ]
private string MakeSuccessResponse(object data, string msg = "操作成功")
{
var response = new StandardApiResponse
{
Success = true,
Message = msg,
Data = data
};
return JsonSerializer.Serialize(response, _jsonOptions);
}
private string MakeErrorResponse(string msg)
{
var response = new StandardApiResponse
{
Success = false,
Message = msg,
Data = null
};
return JsonSerializer.Serialize(response, _jsonOptions);
}
#endregion
}
}
@@ -0,0 +1,51 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using WebView2Demo.Models;
using WebView2Demo.WebInterop.Attributes;
namespace WebView2Demo.WebInterop.Services
{
[WebApiControl]
public class SystemBusinessService
{
/// <summary>
/// 获取系统配置
/// </summary>
[WebAction("GetSystemInfo")]
public object GetInfo()
{
return new
{
OS = Environment.OSVersion.ToString(),
Runtime = "WebView2 .NET 10",
IsAdmin = true
};
}
/// <summary>
/// 执行原生弹窗
/// </summary>
[WebAction("ShowDialog")]
public bool ShowDialog(string message)
{
// 确保在 UI 线程执行
Application.Current.Dispatcher.Invoke(() =>
{
MessageBox.Show(message, "WPF 业务 Service 弹窗");
});
return true;
}
/// <summary>
/// 异步计算演示
/// </summary>
[WebAction("Calculate")]
public async Task<int> CalculateAsync(CalculationParams p)
{
// 模拟业务逻辑耗时
await Task.Delay(500);
return (p?.A ?? 0) + (p?.B ?? 0);
}
}
}
+35 -67
View File
@@ -1,84 +1,52 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useAppLogic } from './AppLogic'
const receivedMsg = ref('等待消息...')
const inputMsg = ref('')
const handleMessage = (event) => {
console.log("收到来自 WPF 的消息:", event.data);
receivedMsg.value = event.data
}
onMounted(() => {
if (window.chrome && window.chrome.webview) {
window.chrome.webview.addEventListener('message', handleMessage)
}
})
onUnmounted(() => {
if (window.chrome && window.chrome.webview) {
window.chrome.webview.removeEventListener('message', handleMessage)
}
})
const sendMessageToWPF = () => {
if (window.chrome && window.chrome.webview) {
window.chrome.webview.postMessage(inputMsg.value)
} else {
alert('未在 WebView2 环境中运行')
}
}
// 所有的业务逻辑都在 AppLogic.js 中实现
const {
receivedMsg,
systemInfo,
calcResult,
fetchSystemInfo,
triggerNativeAlert,
runCalculation
} = useAppLogic()
</script>
<template>
<div class="container">
<h1>Vue 3 + WebView2 交互演示</h1>
<h1>WebView2 标准化交互网关</h1>
<div class="card">
<h3>接收来自 WPF 的消息:</h3>
<p class="msg-box">{{ receivedMsg }}</p>
<h3>1. 属性同步 (WebMessage)</h3>
<p>来自 WPF 的推送: {{ receivedMsg }}</p>
</div>
<div class="card">
<h3>发送消息给 WPF:</h3>
<input v-model="inputMsg" placeholder="输入消息" />
<button @click="sendMessageToWPF">发送到 WPF</button>
<h3>2. 动态 API 调用 (Host Objects + 信封)</h3>
<div class="btn-group">
<button @click="fetchSystemInfo">获取 C# 系统信息</button>
<button @click="triggerNativeAlert" class="btn-warning">触发原生弹窗</button>
<button @click="runCalculation" class="btn-info">计算 10 + 20</button>
</div>
<div v-if="systemInfo" class="result-box">
<b>系统信息:</b> {{ systemInfo.OS }} ({{ systemInfo.Runtime }})
</div>
<div v-if="calcResult !== null" class="result-box">
<b>计算结果:</b> {{ calcResult }}
</div>
</div>
</div>
</template>
<style scoped>
.container {
font-family: Arial, sans-serif;
padding: 20px;
text-align: center;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
background: #f9f9f9;
}
.msg-box {
font-weight: bold;
color: #2c3e50;
font-size: 1.2em;
}
input {
padding: 8px;
width: 200px;
margin-right: 10px;
}
button {
padding: 8px 15px;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #3aa876;
}
.container { padding: 20px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
.card { border: 1px solid #eee; padding: 15px; margin-bottom: 15px; border-radius: 8px; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.btn-group { display: flex; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
button { padding: 8px 16px; border: none; border-radius: 4px; background: #42b883; color: white; cursor: pointer; }
button:hover { background: #3aa876; }
.btn-warning { background: #f39c12; }
.btn-info { background: #3498db; }
.result-box { margin-top: 15px; padding: 10px; background: #f8f9fa; border-left: 4px solid #42b883; font-size: 0.9em; }
</style>
+60
View File
@@ -0,0 +1,60 @@
import { ref, onMounted } from 'vue'
import { InteropHelper } from './utils/interop'
/**
* App 页面的业务逻辑 Hook
*/
export function useAppLogic() {
const receivedMsg = ref('等待消息...')
const systemInfo = ref(null)
const calcResult = ref(null)
// 1. 获取系统信息 (演示带返回值的标准化调用)
const fetchSystemInfo = async () => {
try {
const data = await InteropHelper.callApi('GetSystemInfo')
systemInfo.value = data
} catch (err) {
alert('获取失败: ' + err.message)
}
}
// 2. 发送弹窗指令 (演示带参数的标准化调用)
const triggerNativeAlert = async () => {
try {
await InteropHelper.callApi('ShowDialog', '这是来自 Vue 的标准化信封消息!')
} catch (err) {
console.error('弹窗指令发送失败:', err)
}
}
// 3. 泛型计算演示 (演示复杂对象参数)
const runCalculation = async () => {
try {
const result = await InteropHelper.callApi('Calculate', { a: 10, b: 20 })
calcResult.value = result
} catch (err) {
console.error('计算失败:', err)
}
}
// 原有的 WebMessage 接收逻辑
const handleMessage = (event) => {
receivedMsg.value = event.data
}
onMounted(() => {
if (window.chrome && window.chrome.webview) {
window.chrome.webview.addEventListener('message', handleMessage)
}
})
return {
receivedMsg,
systemInfo,
calcResult,
fetchSystemInfo,
triggerNativeAlert,
runCalculation
}
}
+47
View File
@@ -0,0 +1,47 @@
/**
* 前后端互操作工具类
*/
export class InteropHelper {
/**
* 调用原生 API (通过 StandardApiRequest/Response 协议)
* @param {string} action 行为名称
* @param {any} data 业务数据
* @returns {Promise<any>}
*/
static async callApi(action, data = null) {
if (!window.chrome?.webview?.hostObjects?.gateway) {
const errorMsg = 'WebView2 环境未就绪 (hostObjects.gateway 缺失)';
console.error(errorMsg);
throw new Error(errorMsg);
}
const gateway = window.chrome.webview.hostObjects.gateway;
// 1. 封装标准信封
const requestEnvelope = {
action: action,
data: data
};
try {
// 2. 调用 C# 网关方法 (使用新的方法名 InvokeAction)
const responseJson = await gateway.InvokeAction(JSON.stringify(requestEnvelope));
console.log(`API [${action}] 原始响应:`, responseJson);
// 3. 解析标准回包
const response = JSON.parse(responseJson);
if (response.success) {
return response.data;
} else {
const errorMsg = response.message || '未知错误';
console.error(`API [${action}] 返回失败:`, errorMsg);
throw new Error(errorMsg);
}
} catch (err) {
console.error(`API 调用崩溃 [${action}]:`, err);
// 如果是在控制台看到的报错,通常是因为 C# 端发生了未捕获异常
throw err;
}
}
}