一、DotNetObjectReference:将C#对象暴露给JavaScript
上一章我们完成了"C#调用JavaScript"这个方向,本章转向另一个方向:让JavaScript主动调用C#方法。这在集成事件驱动的JavaScript库时非常关键------当第三方库内部发生某个事件(如图表被点击、编辑器内容变化、定时任务触发),它需要一种机制来通知Blazor组件。
核心工具是DotNetObjectReference<T>,它将一个C#对象包装成一个可被JavaScript引用的句柄,JS侧通过这个句柄调用对象上标记了[JSInvokable]的公共方法。
DotNetObjectReference的典型生命周期是:在OnAfterRenderAsync中创建并传递给JS,在Dispose中释放。释放是必须的------不释放会导致.NET侧的对象被GC Roots所持有,永远无法被垃圾回收:
csharp
// 生命周期管理的标准模式
private DotNetObjectReference<MyComponent>? dotNetRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// 创建对当前组件实例的引用
dotNetRef = DotNetObjectReference.Create(this);
// 将引用传给JS,JS持有这个句柄
await JS.InvokeVoidAsync("initializeWithCallback", dotNetRef);
}
}
public async ValueTask DisposeAsync()
{
// 释放引用,允许C#对象被垃圾回收
dotNetRef?.Dispose();
}
二、[JSInvokable]:可被JavaScript调用的C#方法
在C#类中,任何需要被JavaScript调用的公共方法 都必须标记[JSInvokable]特性。默认情况下,JavaScript端调用时使用的方法名与C#方法名相同(大小写敏感),也可以通过[JSInvokable("customName")]自定义:
csharp
// JavaScript 调用时使用 "UpdateData"
[JSInvokable]
public void UpdateData(string newValue)
{
currentValue = newValue;
StateHasChanged(); // 通知Blazor重渲染
}
// JavaScript 调用时使用 "handleResult"(自定义名称)
[JSInvokable("handleResult")]
public async Task<string> ProcessResult(int code)
{
var result = await SomeService.ProcessAsync(code);
return result.ToString();
}
[JSInvokable]方法可以有参数(JSON自动反序列化)和返回值(JSON自动序列化),也可以是async Task或async Task<T>------JavaScript侧调用时返回的是Promise,可以正常await。
除了通过DotNetObjectReference调用实例方法,[JSInvokable]也可以标记静态方法,此时不需要传递任何对象引用,JavaScript直接通过程序集名和方法名调用:
csharp
// 静态方法:JS调用 DotNet.invokeMethodAsync('MyApp', 'GetAppVersion')
[JSInvokable]
public static string GetAppVersion()
{
return "1.2.3";
}
三、实战:集成Chart.js构建交互式图表
理论的最佳检验是实践。我们来完整实现一个集成了Chart.js的动态折线图组件,它支持从C#端动态更新数据,并在用户点击图表数据点时,将点击信息回调给Blazor组件:
第一步:在HTML中引入Chart.js
在App.razor的<head>中引入Chart.js CDN(生产环境建议下载到本地wwwroot):
html
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script type="module" src="/js/chartInterop.js"></script>
这里注意type="module"------我们的互操作代码使用ES模块语法,必须以module方式加载。
第二步:编写JavaScript互操作模块
javascript
// wwwroot/js/chartInterop.js
// 存储已创建的图表实例,key为canvas元素的id
const charts = new Map();
/**
* 创建折线图
* @param {HTMLElement} canvasElement - canvas DOM元素
* @param {object} chartData - 初始数据 { labels: string[], datasets: object[] }
* @param {object} dotNetRef - .NET对象引用,用于回调
*/
export function createLineChart(canvasElement, chartData, dotNetRef) {
const ctx = canvasElement.getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: chartData,
options: {
responsive: true,
animation: { duration: 500 },
plugins: {
legend: { position: 'top' },
},
onClick: async (event, elements) => {
// 用户点击图表时,回调Blazor组件
if (elements.length > 0) {
const element = elements[0];
const datasetIndex = element.datasetIndex;
const dataIndex = element.index;
const value = chart.data.datasets[datasetIndex].data[dataIndex];
const label = chart.data.labels[dataIndex];
// invokeMethodAsync 调用 [JSInvokable] 标记的方法
// 参数按JSON传递,在C#侧自动反序列化
await dotNetRef.invokeMethodAsync('OnChartPointClicked', {
datasetIndex,
dataIndex,
value,
label
});
}
}
}
});
// 使用canvas元素的id作为key存储图表实例
charts.set(canvasElement.id, chart);
}
/**
* 更新图表数据(无需重建整个图表)
* @param {string} canvasId - canvas元素的id
* @param {number[][]} newData - 每个数据集的新数据数组
* @param {string[]} newLabels - 新的X轴标签数组
*/
export function updateChartData(canvasId, newData, newLabels) {
const chart = charts.get(canvasId);
if (!chart) return;
chart.data.labels = newLabels;
newData.forEach((data, index) => {
if (chart.data.datasets[index]) {
chart.data.datasets[index].data = data;
}
});
// 触发图表重绘
chart.update();
}
/**
* 销毁图表,释放Canvas上下文资源
* @param {string} canvasId
*/
export function destroyChart(canvasId) {
const chart = charts.get(canvasId);
if (chart) {
chart.destroy();
charts.delete(canvasId);
}
}
第三步:编写Blazor组件
csharp
@* Components/Shared/LineChart.razor *@
@inject IJSRuntime JS
@implements IAsyncDisposable
<div class="chart-container" style="position: relative; height: 300px;">
@* @ref 获取canvas元素的引用;id用于JS侧存储图表实例 *@
<canvas id="@canvasId" @ref="canvasRef"></canvas>
</div>
@if (lastClickedPoint is not null)
{
<p class="chart-info">
您点击了:<strong>@lastClickedPoint.Label</strong>,值为 <strong>@lastClickedPoint.Value</strong>
</p>
}
@code {
// 接收图表标签和数据集作为参数
[Parameter]
public List<string> Labels { get; set; } = [];
[Parameter]
public List<ChartDataset> Datasets { get; set; } = [];
// 当图表内数据点被点击时,通知父组件
[Parameter]
public EventCallback<ChartPointInfo> OnPointClicked { get; set; }
private ElementReference canvasRef;
// 使用固定的唯一ID,确保多个图表组件共存时不冲突
private readonly string canvasId = $"chart-{Guid.NewGuid():N}";
private IJSObjectReference? jsModule;
private DotNetObjectReference<LineChart>? dotNetRef;
private ChartPointInfo? lastClickedPoint;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// 加载ES模块
jsModule = await JS.InvokeAsync<IJSObjectReference>(
"import", "/js/chartInterop.js");
// 创建当前组件的 .NET 引用,用于JS回调
dotNetRef = DotNetObjectReference.Create(this);
// 构建Chart.js所需的数据格式
var chartData = new
{
labels = Labels,
datasets = Datasets.Select(d => new
{
label = d.Label,
data = d.Data,
borderColor = d.Color,
backgroundColor = d.Color + "33", // 加透明度
tension = 0.4 // 曲线平滑度
})
};
// 调用JS模块中的 createLineChart 函数,传入DOM元素、数据和.NET引用
await jsModule.InvokeVoidAsync("createLineChart", canvasRef, chartData, dotNetRef);
}
}
// 当父组件传入的数据发生变化时,更新图表(无需重建)
protected override async Task OnParametersSetAsync()
{
// jsModule为null说明还没完成首次渲染初始化,跳过
if (jsModule is null) return;
var newData = Datasets.Select(d => d.Data).ToList();
await jsModule.InvokeVoidAsync("updateChartData", canvasId, newData, Labels);
}
// 此方法被 JavaScript 端的 onClick 事件通过 dotNetRef 调用
// 必须是 public,才能被 DotNetObjectReference 反射发现
[JSInvokable]
public async Task OnChartPointClicked(ChartPointInfo pointInfo)
{
lastClickedPoint = pointInfo;
// 触发父组件订阅的回调
await OnPointClicked.InvokeAsync(pointInfo);
// 手动触发重渲染,更新 lastClickedPoint 的显示
StateHasChanged();
}
public async ValueTask DisposeAsync()
{
if (jsModule is not null)
{
// 通知JS侧销毁图表,释放Canvas资源
await jsModule.InvokeVoidAsync("destroyChart", canvasId);
await jsModule.DisposeAsync();
}
// 必须释放,否则 this(组件实例)将被JS侧持有并无法GC
dotNetRef?.Dispose();
}
}
csharp
// 数据模型
public class ChartDataset
{
public string Label { get; set; } = string.Empty;
public List<double> Data { get; set; } = [];
public string Color { get; set; } = "#0078d4";
}
public class ChartPointInfo
{
public int DatasetIndex { get; set; }
public int DataIndex { get; set; }
public double Value { get; set; }
public string Label { get; set; } = string.Empty;
}
第四步:在父页面中使用图表组件
csharp
@page "/dashboard"
<h1>销售仪表盘</h1>
<LineChart Labels="@months" Datasets="@datasets" OnPointClicked="HandlePointClick" />
<button @onclick="AddRandomData">添加随机数据</button>
@if (clickInfo is not null)
{
<p>点击详情:@clickInfo.Label 月,@clickInfo.Value 万元</p>
}
@code {
private List<string> months = ["1月", "2月", "3月", "4月", "5月", "6月"];
private List<ChartDataset> datasets = [
new() { Label = "2024年", Data = [85, 92, 78, 110, 95, 130], Color = "#0078d4" },
new() { Label = "2023年", Data = [72, 80, 65, 88, 76, 105], Color = "#ff6b35" }
];
private ChartPointInfo? clickInfo;
private readonly Random rng = new();
private void HandlePointClick(ChartPointInfo info)
{
clickInfo = info;
}
private void AddRandomData()
{
months.Add($"{months.Count + 1}月");
foreach (var ds in datasets)
{
ds.Data.Add(Math.Round(60 + rng.NextDouble() * 80, 1));
}
}
}
这个完整示例展示了双向互操作的全貌:C#通过jsModule.InvokeVoidAsync创建和更新图表(C#→JS方向),Chart.js通过dotNetRef.invokeMethodAsync回调OnChartPointClicked(JS→C#方向)。两个方向相互配合,让Razor组件与JavaScript库形成了有机的整体。
四、总结
本章完成了JavaScript互操作的反向链路:DotNetObjectReference.Create(this)将C#对象包装为JS可见的引用;[JSInvokable]标记的公共方法可被JS通过dotNetRef.invokeMethodAsync调用;通过Chart.js集成的完整实战,我们演示了从图表创建、数据动态更新到点击回调的全流程,也强调了dotNetRef.Dispose()和jsModule.DisposeAsync()对于防止内存泄漏的重要性。
至此,第三章JavaScript互操作的两个方向均已讲解完毕。一个独立运行的前端离不开数据来源,而数据通常来自后端API。下一章我们进入第四章,探讨如何构建ASP.NET Core Web API服务器,以及Blazor WebAssembly如何通过HttpClient安全、高效地与API进行数据交换。