WPF和web泛型通信
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user