在 ASP.NET Core Blazor 中从 .NET 方法调用 JavaScript 函数

本文介绍如何从 .NET 调用 JavaScript (JS) 函数。 有关如何从 JS 调用 .NET 方法的信息,请参阅 从 ASP.NET Core Blazor 中的 JavaScript 函数调用 .NET 方法

若要从 .NET 调入 JS,请注入 IJSRuntime 抽象并调用以下方法之一:

对于上述调用 JS 函数的 .NET 方法:

  • 函数标识符 (String) 相对于全局范围 (window)。 若要调用 window.someScope.someFunction,则标识符为 someScope.someFunction。 无需在调用函数之前进行注册。
  • Object[] 中任意数量的 JSON-serializable 参数传递到 JS 函数。
  • 取消标记 (CancellationToken) 对应该取消操作的通知进行传播。
  • TimeSpan 表示 JS 操作的时间限制。
  • 返回类型 TValue 也必须可进行 JSON 序列化。 TValue 应该与最能映射到所返回 JSON 类型的 .NET 类型匹配。
  • InvokeAsync 方法返回 JS PromiseInvokeAsync 会将 Promise 解包并返回 Promise 所等待的值。

对于启用了预呈现的 Blazor Server 应用,初始预呈现期间无法调入 JS。 在建立与浏览器的连接之后,必须延迟 JS 互操作调用。 有关详细信息,请参阅检测 Blazor Server 应用进行预呈现的时间部分。

下面的示例基于 TextDecoder(一种基于 JS 的解码器)。 此示例展示了如何通过 C# 方法调用 JS 函数,以将要求从开发人员代码卸载到现 JS API。 JS 函数从 C# 方法接受字节数组,对数组进行解码,并将文本返回给组件进行显示。

将以下 JS 代码添加到 wwwroot/index.html 的右 </body> 标记 (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor Server):

<script>
  window.convertArray = (win1251Array) => {
    var win1251decoder = new TextDecoder('windows-1251');
    var bytes = new Uint8Array(win1251Array);
    var decodedArray = win1251decoder.decode(bytes);
    console.log(decodedArray);
    return decodedArray;
  };
</script>

以下 CallJsExample1 组件:

  • 选择按钮 (Convert Array) 时,使用 InvokeAsync 调用 convertArray JS 函数。
  • 调用 JS 函数之后,传递的数组会转换为字符串。 该字符串会返回给组件进行显示 (text)。

Pages/CallJsExample1.razor:

@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}
@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new MarkupString(await JS.InvokeAsync<string>("convertArray", 
            quoteArray));
    }
}

调用 JavaScript 函数,而不读取返回的值 (InvokeVoidAsync)

在以下情况下使用 InvokeVoidAsync

wwwroot/index.html 的右 </body> 标记 (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor Server) 中,提供 displayTickerAlert1 JS 函数。 该函数通过 InvokeVoidAsync 进行调用,不返回值:

<script>
  window.displayTickerAlert1 = (symbol, price) => {
    alert(`${symbol}: $${price}!`);
  };
</script>

组件 (.razor) 示例 (InvokeVoidAsync)

TickerChanged 调用以下 CallJsExample2 组件中的 handleTickerChanged1 方法。

Pages/CallJsExample2.razor:

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}
@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

类 (.cs) 示例 (InvokeVoidAsync)

JsInteropClasses1.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses1
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses1
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged 调用以下 CallJsExample3 组件中的 handleTickerChanged1 方法。

Pages/CallJsExample3.razor:

@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses1 jsClass;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await jsClass.TickerChanged(stockSymbol, price);
    }

    public void Dispose() => jsClass?.Dispose();
}
@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses1 jsClass;

    protected override void OnInitialized()
    {
        jsClass = new JsInteropClasses1(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await jsClass.TickerChanged(stockSymbol, price);
    }

    public void Dispose() => jsClass?.Dispose();
}

调用 JavaScript 函数并读取返回的值 (InvokeAsync)

当 .NET 应读取 JS 调用的结果时,使用 InvokeAsync

wwwroot/index.html 的右 </body> 标记 (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor Server) 中,提供 displayTickerAlert2 JS 函数。 下面的示例返回一个字符串,以供调用方显示:

<script>
  window.displayTickerAlert2 = (symbol, price) => {
    if (price < 20) {
      alert(`${symbol}: $${price}!`);
      return "User alerted in the browser.";
    } else {
      return "User NOT alerted.";
    }
  };
</script>

组件 (.razor) 示例 (InvokeAsync)

TickerChanged 调用 handleTickerChanged2 方法,并显示以下 CallJsExample4 组件中返回的字符串。

Pages/CallJsExample4.razor:

@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private string result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}
@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result != null)
{
    <p>@result</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private string result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}

类 (.cs) 示例 (InvokeAsync)

JsInteropClasses2.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses2
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses2
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged 调用 handleTickerChanged2 方法,并显示以下 CallJsExample5 组件中返回的字符串。

Pages/CallJsExample5.razor:

@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses2 jsClass;
    private string result;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = await jsClass.TickerChanged(stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }

    public void Dispose() => jsClass?.Dispose();
}
@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result != null)
{
    <p>@result</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses2 jsClass;
    private string result;

    protected override void OnInitialized()
    {
        jsClass = new JsInteropClasses2(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = await jsClass.TickerChanged(stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }

    public void Dispose() => jsClass?.Dispose();
}

动态内容生成方案

对于使用 BuildRenderTree 的动态内容生成,请使用 [Inject] 属性:

[Inject]
IJSRuntime JS { get; set; }

检测 Blazor Server 应用进行预呈现的时间

本部分适用于预呈现 Razor 组件的 Blazor Server 和托管 Blazor WebAssembly 应用。预呈现包含在 预呈现和集成 ASP.NET Core Razor 组件 中。

在应用进行预呈现时,无法执行调用 JavaScript 等特定操作。 预呈现时,组件可能需要进行不同的呈现。

对于以下示例,setElementText1 函数置于 wwwroot/index.html<head> 元素 (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor Server) 内部。 该函数通过 JSRuntimeExtensions.InvokeVoidAsync 进行调用,不返回值:

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

警告

上述示例直接修改文档对象模型 (DOM),以便仅供演示所用。 大多数情况下,不建议使用 JavaScript 直接修改 DOM,因为 JavaScript 可能会干扰 Blazor 的更改跟踪。 有关详细信息,请参阅 Blazor JavaScript 互操作性(JS 互操作)

若要将 JavaScript 互操作调用延迟到能够保证此类调用正常工作之后,请重写 OnAfterRender{Async} 生命周期事件。 仅在完全呈现应用后,才会调用此事件。

Pages/PrerenderedInterop1.razor:

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}
@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

备注

前面的示例使用全局方法来污染客户端。 若要在生产应用中获取更好的方法,请参阅 JavaScript 模块中的 JavaScript 隔离

示例:

export setElementText1 = (element, text) => element.innerText = text;

以下组件展示了如何以一种与预呈现兼容的方式将 JavaScript 互操作用作组件初始化逻辑的一部分。 该组件显示可从 OnAfterRenderAsync 内部触发呈现更新。 开发人员必须小心避免在此场景中创建无限循环。

对于以下示例,setElementText2 函数置于 wwwroot/index.html<head> 元素 (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor Server) 内部。 该函数通过 IJSRuntime.InvokeAsync 进行调用,会返回值:

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

警告

上述示例直接修改文档对象模型 (DOM),以便仅供演示所用。 大多数情况下,不建议使用 JavaScript 直接修改 DOM,因为 JavaScript 可能会干扰 Blazor 的更改跟踪。 有关详细信息,请参阅 Blazor JavaScript 互操作性(JS 互操作)

如果调用 JSRuntime.InvokeAsync,则 ElementReference 仅在 OnAfterRenderAsync 中使用,而不在任何更早的生命周期方法中使用,因为呈现组件后才会有 JavaScript 元素。

通过调用 StateHasChanged,可使用从 JavaScript 互操作调用中获取的新状态重新呈现组件(有关详细信息,请参阅 ASP.NET Core Blazor 组件呈现)。 此代码不会创建无限循环,因为仅在 datanull 时才调用 StateHasChanged

Pages/PrerenderedInterop2.razor:

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string data;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && data == null)
        {
            data = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}
@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(infoFromJs ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string infoFromJs;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && infoFromJs == null)
        {
            infoFromJs = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

备注

前面的示例使用全局方法来污染客户端。 若要在生产应用中获取更好的方法,请参阅 JavaScript 模块中的 JavaScript 隔离

示例:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

JavaScipt 的位置

使用 JavaScript (JS) 互操作性(互操作)概述文章中所述的任何方法加载 JavaScript (JS) 代码:

有关在 JS 模块中隔离脚本的信息,请参阅 JavaScript 模块中的 JavaScript 隔离部分。

警告

请勿将 <script> 标记置于组件文件 (.razor) 中,因为 <script> 标记无法动态更新。

JavaScript 模块中的 JavaScript 隔离

Blazor 在标准 JavaScript 模块ECMAScript 规范)中启用 JavaScript (JS) 隔离。

JS 隔离具有以下优势:

  • 导入的 JS 不再污染全局命名空间。
  • 库和组件的使用者不需要导入相关的 JS。

例如,以下 JS 模块导出用于显示浏览器窗口提示的 JS 函数。 将以下 JS 代码置于外部 JS 文件中。

wwwroot/scripts.js:

export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

将前面的 JS 模块作为 wwwroot 文件夹中的静态 Web 资产添加到应用或类库中,然后通过调用 IJSRuntime 实例上的 InvokeAsync 将该模块导入 .NET 代码。

IJSRuntime 将模块作为 IJSObjectReference 导入,它表示对 .NET 代码中 JS 对象的引用。 使用 IJSObjectReference 调用从模块导出的 JS 函数。

Pages/CallJsExample6.razor:

@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference module;
    private string result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string> Prompt(string message)
    {
        return await module.InvokeAsync<string>("showPrompt", message);
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

在上面的示例中:

  • 按照约定,import 标识符是专门用于导入 JS 模块的特殊标识符。
  • 使用稳定的静态 Web 资产路径 ./{SCRIPT PATH AND FILENAME (.js)} 指定该模块的外部 JS 文件,其中:
    • 若要创建 JS 文件的正确静态资产路径,需要当前目录 (./) 的路径段。
    • {SCRIPT PATH AND FILENAME (.js)} 占位符是 wwwroot 下的路径和文件名。

动态导入模块需要网络请求,因此只能通过调用 InvokeAsync 来异步实现。

IJSInProcessObjectReference 表示对某个 JS 对象的引用,该对象的函数可以同步进行调用。

备注

当外部 JS 文件由 Razor 类库提供时,使用其稳定的静态 Web 资产路径 ./_content/{LIBRARY NAME}/{SCRIPT PATH AND FILENAME (.js)} 指定模块的 JS 文件:

  • 若要创建 JS 文件的正确静态资产路径,需要当前目录 (./) 的路径段。
  • 占位符 {LIBRARY NAME} 是库的名称。 在以下示例中,库名称为 ComponentLibrary
  • {SCRIPT PATH AND FILENAME (.js)} 占位符是 wwwroot 下的路径和文件名。 在以下实例中,外部 JS 文件 (script.js) 置于类库的 wwwroot 文件夹中。
var module = await js.InvokeAsync<IJSObjectReference>(
    "import", "./_content/ComponentLibrary/scripts.js");

有关详细信息,请参阅 使用 Razor 类库中的 ASP.NET Core Razor 组件

捕获对元素的引用

某些 JavaScript (JS) 互操作方案需要引用 HTML 元素。 例如,一个 UI 库可能需要用于初始化的元素引用,或者你可能需要对元素调用类似于命令的 API(如 clickplay)。

使用以下方法在组件中捕获对 HTML 元素的引用:

  • 向 HTML 元素添加 @ref 属性。
  • 定义一个类型为 ElementReference 字段,其名称与 @ref 属性的值匹配。

以下示例演示如何捕获对 username <input> 元素的引用:

<input @ref="username" ... />

@code {
    private ElementReference username;
}

警告

只使用元素引用改变不与 Blazor 交互的空元素的内容。 当第三方 API 向元素提供内容时,此方案十分有用。 由于 Blazor 不与元素交互,因此在 Blazor 的元素表示形式与文档对象模型 (DOM) 之间不可能存在冲突。

在下面的示例中,改变无序列表 (ul) 的内容具有危险性,因为 Blazor 会与 DOM 交互以填充此来自 Todos 对象的元素的列表项 (<li>):

<ul @ref="MyList">
    @foreach (var item in Todos)
    {
        <li>@item.Text</li>
    }
</ul>

如果 JS 互操作改变元素 MyList 的内容,并且 Blazor 尝试将差异应用于元素,则差异与 DOM 不匹配。

有关详细信息,请参阅 Blazor JavaScript 互操作性(JS 互操作)

通过 JS 互操作将 ElementReference 传递给 JS 代码。 JS 代码会收到一个 HTMLElement 实例,该实例可以与常规 DOM API 一起使用。 例如,下面的代码定义了一个 .NET 扩展方法 (TriggerClickEvent),该方法的作用是将鼠标单击事件发送到某个元素。

JS 函数 clickElement 在已传递的 HTML 元素 (element) 上创建 click 事件:

window.interopFunctions = {
  clickElement : function (element) {
    element.click();
  }
}

若要调用不返回值的 JS 函数,请使用 JSRuntimeExtensions.InvokeVoidAsync。 下面的代码使用捕获的 click 调用上面的 JS 函数,触发客户端 ElementReference 事件:

@inject IJSRuntime JS

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await JS.InvokeVoidAsync(
            "interopFunctions.clickElement", exampleButton);
    }
}

若要使用扩展方法,请创建接收 IJSRuntime 实例的静态扩展方法:

public static async Task TriggerClickEvent(this ElementReference elementRef, 
    IJSRuntime js)
{
    await js.InvokeVoidAsync("interopFunctions.clickElement", elementRef);
}

clickElement 方法在对象上直接调用。 下面的示例假设可从 JsInteropClasses 命名空间使用 TriggerClickEvent 方法:

@inject IJSRuntime JS
@using JsInteropClasses

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await exampleButton.TriggerClickEvent(JS);
    }
}

重要

仅在呈现组件后填充 exampleButton 变量。 如果将未填充的 ElementReference 传递给 JS 代码,则 JS 代码会收到 null 值。 若要在组件完成呈现后操作元素引用,请使用 OnAfterRenderAsyncOnAfterRender 组件生命周期方法

使用泛型类型并返回值时,请使用 ValueTask<TResult>

public static ValueTask<T> GenericMethod<T>(this ElementReference elementRef, 
    IJSRuntime js)
{
    return js.InvokeAsync<T>("{JAVASCRIPT FUNCTION}", elementRef);
}

{JAVASCRIPT FUNCTION} 占位符是 JS 函数标识符。

GenericMethod 在具有类型的对象上直接调用。 下面的示例假设可从 JsInteropClasses 命名空间使用 GenericMethod

@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}

跨组件引用元素

不能在组件之间传递 ElementReference,因为:

若要使父组件可以向其他组件提供元素引用,父组件可以:

  • 允许子组件注册回调。
  • OnAfterRender 事件期间,通过传递的元素引用调用注册的回调。 此方法间接地允许子组件与父级的元素引用交互。

将以下样式添加到 wwwroot/index.html<head> (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor Server):

<style>
    .red { color: red }
</style>

将以下脚本添加到 wwwroot/index.html 的右 </body> 标记 (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor Server) 中:

<script>
  function setElementClass(element, className) {
    var myElement = element;
    myElement.classList.add(className);
  }
</script>

Pages/CallJsExample7.razor(父组件):

@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="@this" Title="How is Blazor working for you?" />
@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="@this" Title="How is Blazor working for you?" />

Pages/CallJsExample7.razor.cs:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages
{
    public partial class CallJsExample7 : 
        ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private IList<IObserver<ElementReference>> subscriptions = 
            new List<IObserver<ElementReference>>();
        private ElementReference title;

        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnNext(title);
                }
                catch (Exception)
                {
                    throw;
                }
            }
        }

        public void Dispose()
        {
            disposing = true;

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnCompleted();
                }
                catch (Exception)
                {
                }
            }

            subscriptions.Clear();
        }

        public IDisposable Subscribe(IObserver<ElementReference> observer)
        {
            if (disposing)
            {
                throw new InvalidOperationException("Parent being disposed");
            }

            subscriptions.Add(observer);

            return new Subscription(observer, this);
        }

        private class Subscription : IDisposable
        {
            public Subscription(IObserver<ElementReference> observer, 
                CallJsExample7 self)
            {
                Observer = observer;
                Self = self;
            }

            public IObserver<ElementReference> Observer { get; }
            public CallJsExample7 Self { get; }

            public void Dispose()
            {
                Self.subscriptions.Remove(Observer);
            }
        }
    }
}
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages
{
    public partial class CallJsExample7 : 
        ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private IList<IObserver<ElementReference>> subscriptions = 
            new List<IObserver<ElementReference>>();
        private ElementReference title;

        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnNext(title);
                }
                catch (Exception)
                {
                    throw;
                }
            }
        }

        public void Dispose()
        {
            disposing = true;

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnCompleted();
                }
                catch (Exception)
                {
                }
            }

            subscriptions.Clear();
        }

        public IDisposable Subscribe(IObserver<ElementReference> observer)
        {
            if (disposing)
            {
                throw new InvalidOperationException("Parent being disposed");
            }

            subscriptions.Add(observer);

            return new Subscription(observer, this);
        }

        private class Subscription : IDisposable
        {
            public Subscription(IObserver<ElementReference> observer, 
                CallJsExample7 self)
            {
                Observer = observer;
                Self = self;
            }

            public IObserver<ElementReference> Observer { get; }
            public CallJsExample7 Self { get; }

            public void Dispose()
            {
                Self.subscriptions.Remove(Observer);
            }
        }
    }
}

在上一个示例中,应用的命名空间是 BlazorSample,组件位于 Pages 文件夹中。 如果在本地测试代码,请更新命名空间。

Shared/SurveyPrompt.razor(子组件):

@inject IJSRuntime JS

<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    [Parameter]
    public string Title { get; set; }
}
@inject IJSRuntime JS

<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    [Parameter]
    public string Title { get; set; }
}

Shared/SurveyPrompt.razor.cs:

using System;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Shared
{
    public partial class SurveyPrompt : 
        ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable subscription = null;

        [Parameter]
        public IObservable<ElementReference> Parent { get; set; }

        protected override void OnParametersSet()
        {
            base.OnParametersSet();

            subscription?.Dispose();
            subscription = Parent.Subscribe(this);
        }

        public void OnCompleted()
        {
            subscription = null;
        }

        public void OnError(Exception error)
        {
            subscription = null;
        }

        public void OnNext(ElementReference value)
        {
            JS.InvokeAsync<object>(
                "setElementClass", new object[] { value, "red" });
        }

        public void Dispose()
        {
            subscription?.Dispose();
        }
    }
}
using System;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Shared
{
    public partial class SurveyPrompt : 
        ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable subscription = null;

        [Parameter]
        public IObservable<ElementReference> Parent { get; set; }

        protected override void OnParametersSet()
        {
            base.OnParametersSet();

            subscription?.Dispose();
            subscription = Parent.Subscribe(this);
        }

        public void OnCompleted()
        {
            subscription = null;
        }

        public void OnError(Exception error)
        {
            subscription = null;
        }

        public void OnNext(ElementReference value)
        {
            JS.InvokeAsync<object>(
                "setElementClass", new object[] { value, "red" });
        }

        public void Dispose()
        {
            subscription?.Dispose();
        }
    }
}

在上一个示例中,应用的命名空间是 BlazorSample,共享组件位于 Shared 文件夹中。 如果在本地测试代码,请更新命名空间。

强化 JavaScript 互操作调用

本部分主要适用于 Blazor Server 应用,但如果条件允许,Blazor WebAssembly 应用也可以设置 JS 互操作超时。

在 Blazor Server 应用中,JavaScript (JS) 互操作可能会由于网络错误而失败,因此应视为不可靠。 默认情况下,Blazor Server 应用对 JS 互操作调用使用一分钟超时。 如果应用可以容忍更激进的超时,请使用以下方法之一设置超时。

通过 CircuitOptions.JSInteropDefaultCallTimeout 使用 Startup.csStartup.ConfigureServices 方法设置全局超时:

services.AddServerSideBlazor(
    options => options.JSInteropDefaultCallTimeout = {TIMEOUT});

{TIMEOUT} 占位符是TimeSpan(例如 TimeSpan.FromSeconds(80))。

在组件代码中设置每个调用超时。 指定的超时将替代 JSInteropDefaultCallTimeout 设置的全局超时:

var result = await JS.InvokeAsync<string>("{ID}", {TIMEOUT}, new[] { "Arg1" });

在上面的示例中:

  • {TIMEOUT} 占位符是TimeSpan(例如 TimeSpan.FromSeconds(80))。
  • {ID} 占位符是函数要调用的标识符。 例如,值 someScope.someFunction 调用函数 window.someScope.someFunction

尽管 JS 互操作失败的一个常见原因是 Blazor Server 应用中的网络故障,但可以针对 Blazor WebAssembly 应用中的 JS 互操作调用设置每个调用超时。 尽管 Blazor WebAssembly 应用中不存在 SignalR 线路,但 JS 互操作调用可能会由于在 Blazor WebAssembly 应用中应用的其他原因而失败。

有关资源耗尽的详细信息,请参阅 ASP.NET Core Blazor Server 的威胁缓解指南

避免循环引用对象

不能在客户端上针对以下调用就包含循环引用的对象进行序列化:

  • .NET 方法调用。
  • 返回类型具有循环引用时,从 C# 发出的 JavaScript 方法调用。

呈现 UI 的 JavaScript 库

有时你可能需要使用在浏览器文档对象模型 (DOM) 内生成可见用户界面元素的 JavaScript 库 (JS)。 乍一想,这似乎很难,因为 Blazor 的 diffing 系统依赖于对 DOM 元素树的控制,并且如果某个外部代码使 DOM 树发生变化并为了应用 diff 而使其机制失效,就会产生错误。 这并不是一个特定于 Blazor 的限制。 任何基于 diff 的 UI 框架都会面临同样的问题。

幸运的是,将外部生成的 UI 可靠地嵌入到 Razor 组件 UI 非常简单。 推荐的方法是让组件的代码(.razor 文件)生成一个空元素。 就 Blazor 的 diffing 系统而言,该元素始终为空,这样呈现器就不会递归到元素中,而是保留元素内容不变。 这样就可以安全地用外部托管的任意内容来填充元素。

以下示例演示了这一概念。 在 if 语句中,当 firstRendertrue 时,使用 JS 互操作与 Blazor 外的 unmanagedElement 进行交互。 例如,调用某个外部 JS 库来填充元素。 Blazor 保留元素内容不变,直到此组件被删除。 删除组件时,会同时删除组件的整个 DOM 子树。

<h1>Hello! This is a Blazor component rendered at @DateTime.Now</h1>

<div @ref="unmanagedElement"></div>

@code {
    private HtmlElement unmanagedElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            ...
        }
    }
}

请思考以下使用开源 Mapbox API 呈现交互式地图的示例。

以下 JS 模块放置在应用中,或从 Razor 类库中提供。

备注

若要创建 Mapbox 地图,请从 Mapbox 登录获取访问令牌,并在以下代码中显示 {ACCESS TOKEN} 的位置提供它。

wwwroot/mapComponent.js:

import 'https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js';

mapboxgl.accessToken = '{ACCESS TOKEN}';

export function addMapToElement(element) {
  return new mapboxgl.Map({
    container: element,
    style: 'mapbox://styles/mapbox/streets-v11',
    center: [-74.5, 40],
    zoom: 9
  });
}

export function setMapCenter(map, latitude, longitude) {
  map.setCenter([longitude, latitude]);
}

为生成正确的样式,将以下样式表标记添加到主机 HTML 页面中。

wwwroot/index.html (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor Server) 中,将以下 <link> 元素添加到 <head> 元素标记:

<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" 
    rel="stylesheet" />

Pages/CallJsExample8.razor:

@page "/call-js-example-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>

@code
{
    private ElementReference mapElement;
    private IJSObjectReference mapModule;
    private IJSObjectReference mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./mapComponent.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "addMapToElement", mapElement);
        }
    }

    private async Task ShowAsync(double latitude, double longitude)
        => await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, latitude, 
            longitude).AsTask();

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (mapInstance is not null)
        {
            await mapInstance.DisposeAsync();
        }

        if (mapModule is not null)
        {
            await mapModule.DisposeAsync();
        }
    }
}

前面的示例会生成一个交互式地图 UI。 用户:

  • 拖动鼠标可以滚动或缩放。
  • 选择相关按钮可跳转到预定义的位置。

Mapbox 的日本东京街道地图,其中设有可用于选择英国布里斯托尔和日本东京的按钮

在上面的示例中:

  • 就 Blazor 而言,具有 @ref="mapElement"<div> 保留为空。 mapbox-gl.js 脚本可以安全地填充元素并修改其内容。 将此方法与呈现 UI 的任何 JS 库配合使用。 可以在 Blazor 组件中嵌入第三方 JS SPA 框架中的组件,只要它们不会尝试访问和改变页面的其他部分。 对于外部 JS 代码,修改 Blazor 不视为空元素的元素是不安全的。
  • 使用此方法时,请记得有关 Blazor 保留或销毁 DOM 元素的方式的规则。 组件之所以能够安全处理按钮单击事件并更新现有的地图实例,是因为默认在可能的情况下保留 DOM 元素。 如果之前要从 @foreach 循环内呈现地图元素的列表,则需要使用 @key 来确保保留组件实例。 否则,列表数据的更改可能导致组件实例以不合适的方式保留以前实例的状态。 有关详细信息,请参阅使用 @key 保留元素和组件
  • 该示例在 ES6 模块中封装 JS 逻辑和依赖项,并使用 import 标识符动态加载模块。 有关详细信息,请参阅 JavaScript 模块中的 JavaScript 隔离

对 JavaScript 互操作调用的大小限制

This section only applies to Blazor Server apps. In Blazor WebAssembly, the framework doesn't impose a limit on the size of JavaScript (JS) interop inputs and outputs.

In Blazor Server, JS interop calls are limited in size by the maximum incoming SignalR message size permitted for hub methods, which is enforced by HubOptions.MaximumReceiveMessageSize (default: 32 KB). JS to .NET SignalR messages larger than MaximumReceiveMessageSize throw an error. The framework doesn't impose a limit on the size of a SignalR message from the hub to a client.

When SignalR logging isn't set to Debug or Trace, a message size error only appears in the browser's developer tools console:

Error: Connection disconnected with error 'Error: Server returned an error on close: Connection closed with an error.'.

When SignalR server-side logging is set to Debug or Trace, server-side logging surfaces an InvalidDataException for a message size error.

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

Error:

System.IO.InvalidDataException: The maximum message size of 32768B was exceeded. The message size can be configured in AddHubOptions.

Increase the limit by setting MaximumReceiveMessageSize in Startup.ConfigureServices. The following example sets the maximum receive message size to 64 KB (64 * 1024):

services.AddServerSideBlazor()
   .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

Increasing the SignalR incoming message size limit comes at the cost of requiring more server resources, and it exposes the server to increased risks from a malicious user. Additionally, reading a large amount of content in to memory as strings or byte arrays can also result in allocations that work poorly with the garbage collector, resulting in additional performance penalties.

One option for reading large payloads is to send the content in smaller chunks and process the payload as a Stream. This can be used when reading large JSON payloads or if data is available in JS as raw bytes. For an example that demonstrates sending large binary payloads in Blazor Server that uses techniques similar to the InputFile component, see the Binary Submit sample app.

备注

指向 ASP.NET Core 参考源的文档链接会加载存储库的 main 分支的代码,该分支表示产品单元针对下一个 ASP.NET Core 版本的当前开发。 若要为其他版本选择分支,请使用“切换分支或标记”下拉列表来选择分支。 例如,为 ASP.NET Core 5.0 版本选择 release/5.0 分支。

Consider the following guidance when developing code that transfers a large amount of data between JS and Blazor in Blazor Server apps:

  • Slice the data into smaller pieces, and send the data segments sequentially until all of the data is received by the server.
  • Don't allocate large objects in JS and C# code.
  • Don't block the main UI thread for long periods when sending or receiving data.
  • Free consumed memory when the process is completed or cancelled.
  • Enforce the following additional requirements for security purposes:
    • Declare the maximum file or data size that can be passed.
    • Declare the minimum upload rate from the client to the server.
  • After the data is received by the server, the data can be:
    • Temporarily stored in a memory buffer until all of the segments are collected.
    • Consumed immediately. For example, the data can be stored immediately in a database or written to disk as each segment is received.

未封装的 JavaScript 互操作

当针对 JavaScript (JS) 互操作序列化 .NET 对象并且满足以下任一条件时,Blazor WebAssembly 组件的性能可能会较差:

  • 大量 .NET 对象迅速地进行序列化。 例如,通过移动输入设备(如旋转鼠标滚轮)来进行 JS 互操作调用时,可能会导致性能不佳。
  • 对于 JS 互操作,必须序列化大型 .NET 对象或多个 .NET 对象。 例如,JS 互操作调用需要序列化数十个文件时,可能会导致性能不佳。

IJSUnmarshalledObjectReference 表示对某个 JS 对象的引用,该对象的函数无需 .NET 数据序列化开销即可调用。

如下示例中:

  • 包含字符串和整数的 struct 会以非序列化方式传递给 JS。
  • JS 函数处理数据,并将布尔或字符串返回给调用方。
  • JS 字符串无法直接转换为 .NET string 对象。 unmarshalledFunctionReturnString 函数调用 BINDING.js_string_to_mono_string 来管理 JS 字符串的转换。

备注

以下示例不是此方案的典型用例,因为传递给 JS 的 struct 不会导致组件性能变差。 该示例仅使用一个小型对象来演示传递未序列化 .NET 数据的概念。

将以下 <script> 块置于 wwwroot/index.html (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor Server) 中。 或者,你可以通过 <script src="{SCRIPT PATH AND FILE NAME (.js)}></script> 将 JS 放置在右 </body> 标记内引用的外部 JS 文件中,其中 {SCRIPT PATH AND FILE NAME (.js)} 占位符是脚本的路径和文件名。

<script>
  window.returnObjectReference = () => {
    return {
      unmarshalledFunctionReturnBoolean: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return name === "Brigadier Alistair Gordon Lethbridge-Stewart" &&
            year === 1968;
      },
      unmarshalledFunctionReturnString: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return BINDING.js_string_to_mono_string(`Hello, ${name} (${year})!`);
      }
    };
  }
</script>

警告

js_string_to_mono_string 函数的名称、行为和存在可能会在 .NET 的将来版本中更改。 例如:

  • 函数可能已重命名。
  • 函数本身可能会被删除,以便能够通过框架自动转换字符串。

Pages/CallJsExample9.razor:

@page "/call-js-example-9"
@using System.Runtime.InteropServices
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Call JS Example 9</h1>

@if (callResultForBoolean)
{
    <p>JS interop was successful!</p>
}

@if (!string.IsNullOrEmpty(callResultForString))
{
    <p>@callResultForString</p>
}

<p>
    <button @onclick="CallJSUnmarshalledForBoolean">
        Call Unmarshalled JS & Return Boolean
    </button>
    <button @onclick="CallJSUnmarshalledForString">
        Call Unmarshalled JS & Return String
    </button>
</p>

<p>
    <a href="https://www.doctorwho.tv">Doctor Who</a>
    is a registered trademark of the <a href="https://www.bbc.com/">BBC</a>.
</p>

@code {
    private bool callResultForBoolean;
    private string callResultForString;

    private void CallJSUnmarshalledForBoolean()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForBoolean = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, bool>(
                "unmarshalledFunctionReturnBoolean", GetStruct());
    }

    private void CallJSUnmarshalledForString()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForString = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, string>(
                "unmarshalledFunctionReturnString", GetStruct());
    }

    private InteropStruct GetStruct()
    {
        return new InteropStruct
        {
            Name = "Brigadier Alistair Gordon Lethbridge-Stewart",
            Year = 1968,
        };
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct InteropStruct
    {
        [FieldOffset(0)]
        public string Name;

        [FieldOffset(8)]
        public int Year;
    }
}

如果 IJSUnmarshalledObjectReference 实例未使用 C# 代码释放,则可以使用 JS 释放。 以下 dispose 函数在从 JS 调用时释放对象引用:

window.exampleJSObjectReferenceNotDisposedInCSharp = () => {
  return {
    dispose: function () {
      DotNet.disposeJSObjectReference(this);
    },

    ...
  };
}

可以使用 js_typed_array_to_array 将数组类型从 JS 对象转换为 .NET 对象,但 JS 数组必须为类型化数组。 可以使用 C# 代码将 JS 中的数组作为 .NET 对象数组 (object[]) 进行读取。

可以转换其他数据类型(如字符串数组),但需要创建一个新的 Mono 数组对象 (mono_obj_array_new) 并设置其值 (mono_obj_array_set)。

警告

框架提供的 JS 函数(如 Blazor、 js_typed_array_to_arraymono_obj_array_newmono_obj_array_set)可能会在 .NET 的未来版本中进行名称更改、行为更改或删除。

其他资源