You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor中Select2标签控件重复实例事件未触发问题求助

Blazor Select2标签控件事件触发异常问题

问题概述

基于Select2封装了Blazor标签控件,效果如下:
标签组件实现效果
但新增标签时事件触发不符合预期。

预期行为

  • 将「订单编号」设置为O1234
  • 组件的SelectedValuesChanged事件触发,传入O1234
  • 将「客户订单编号」设置为CO1234
  • 组件的SelectedValuesChanged事件触发,传入CO1234

实际行为

  • 将「订单编号」设置为O1234
  • 组件的SelectedValuesChanged事件触发,传入O1234
  • 将「客户订单编号」设置为CO1234
  • 无事件触发

组件代码

C#组件代码(Select2TagSelect.razor)

@inject IJSRuntime JSRuntime
@namespace BillOfLading.UI.Web.Components.Shared
@using Microsoft.JSInterop

<select id="@ElementId" class="form-control select2" multiple="multiple"></select>

@code {
    [Parameter]
    public string ElementId { get; set; } = "select2_" + Guid.NewGuid().ToString("N");

    [Parameter]
    public IList<string> SelectedValues { get; set; } = new List<string>();

    [Parameter]
    public EventCallback<IList<string>> SelectedValuesChanged { get; set; }

    private DotNetObjectReference<Select2TagSelect>? objRef;

    protected override void OnInitialized()
    {
        objRef = DotNetObjectReference.Create(this);
    }

    private async Task OnSelectionChanged(ChangeEventArgs args)
    {
        SelectedValues = await JSRuntime.InvokeAsync<IList<string>>("select2InteropTag.getSelectedValues", ElementId);
        await SelectedValuesChanged.InvokeAsync(SelectedValues);
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JSRuntime.InvokeVoidAsync("select2InteropTag.initializeSelect2Tag", objRef, ElementId);
        }
    }

    [JSInvokable]
    public async void SetSelectedText(IList<string> selected)
    {
        SelectedValues = selected;
        await SelectedValuesChanged.InvokeAsync(SelectedValues);
    }
}

JavaScript互调用代码

window.select2InteropTag = {
    initializeSelect2Tag: function (dotNetHelper, elementId) {
        $('#' + elementId).select2({
            tags: true,
            tokenSeparators: [',', ' ']
        }).on('change', (event) => {
            console.log("Element " + elementId + " has changed");
            var data = $('#' + elementId).select2('data');
            if (data) {
                var value = data.map((element) => element.text);
                dotNetHelper.invokeMethodAsync('SetSelectedText', value);
            }
        });
    }
};

页面渲染代码

<div class="row">
    <div class="mb-2 col-md-6" style="margin-bottom: 0px;">
        <label class="form-label">订单编号</label>
        <Select2TagSelect ElementId="@($"select2-orders-{ShortGuidGenerator.ShortGuid()}")" SelectedValuesChanged="ChangeOrders"></Select2TagSelect>
    </div>
    <div class="mb-2 col-md-6" style="margin-bottom: 0px;">
        <label class="form-label">客户订单编号</label>
        <Select2TagSelect ElementId="@($"select2-customerOrders-{ShortGuidGenerator.ShortGuid()}")" SelectedValuesChanged="ChangeCustomerOrders"></Select2TagSelect>
    </div>
</div>

//... 省略部分代码 ...

@code {
    [Parameter]
    public BillPropertiesViewModel Model { get; set; }

    [Parameter]
    public EventCallback<BillPropertiesViewModel> ModelChanged { get; set; }

    private void ChangeOrders(IList<string> obj)
    {
        Model.Orders = obj;
        ModelChanged.InvokeAsync(Model);
    }

    private void ChangeCustomerOrders(IList<string> obj)
    {
        Model.CustomerOrders = obj;
        ModelChanged.InvokeAsync(Model);
    }
}

异常现象

JavaScript控制台显示两个控件的change事件均已触发(例如输出"Element xyz has changed"),但第二个控件的JS与Blazor C#互调用未生效。

已尝试方案

移除OnAfterRenderAsync中的firstRender判断后,两个组件都能触发事件,但会导致事件重复注册(每次组件重渲染都会注册JS事件),存在性能隐患,不希望采用该方案。

问题原因及解决方案

原因分析

  1. 异步方法类型错误:SetSelectedText使用async void类型,Blazor中异步void方法会脱离组件同步上下文,容易导致调用失效或异常无法捕获。
  2. DotNetObjectReference生命周期管理缺失:未正确释放DotNetObjectReference,当父组件重渲染时,旧组件实例的引用可能被GC回收,导致JS调用C#方法时引用失效。
  3. 动态ElementId导致实例频繁重建:父组件重渲染时,ShortGuidGenerator.ShortGuid()会生成新的ElementId,旧组件实例被销毁,新实例的JS事件注册可能存在上下文衔接问题。

解决方案

1. 修复异步方法类型

将SetSelectedText的返回类型从async void改为async Task,确保Blazor能正确处理异步上下文:

[JSInvokable]
public async Task SetSelectedText(IList<string> selected)
{
    SelectedValues = selected;
    await SelectedValuesChanged.InvokeAsync(SelectedValues);
}

2. 实现IAsyncDisposable管理引用生命周期

添加资源释放逻辑,避免无效引用和内存泄漏:

@implements IAsyncDisposable

// ... 其他代码 ...

public async ValueTask DisposeAsync()
{
    if (objRef != null)
    {
        objRef.Dispose();
        // 销毁Select2实例,清理DOM绑定
        await JSRuntime.InvokeVoidAsync("select2InteropTag.destroySelect2", ElementId);
    }
}

对应的JS补充销毁方法:

window.select2InteropTag = {
    // ... 原有初始化代码 ...
    destroySelect2: function(elementId) {
        const $el = $('#' + elementId);
        if ($el.data('select2')) {
            $el.select2('destroy');
        }
    }
};

3. 优化ElementId生成逻辑

避免父组件重渲染时频繁生成新的ElementId,可基于组件的稳定标识(如绑定数据的唯一ID)生成:

<Select2TagSelect ElementId="@($"select2-orders-{Model.Id}")" SelectedValuesChanged="ChangeOrders"></Select2TagSelect>

4. JS调用添加错误捕获

在invokeMethodAsync后添加错误处理,便于排查问题:

dotNetHelper.invokeMethodAsync('SetSelectedText', value)
    .catch(err => console.error("C#方法调用失败:", err));

关键修复总结

  • 异步JS调用的C#方法必须使用async Task返回类型,禁止async void
  • 实现IAsyncDisposable正确释放DotNetObjectReference和Select2实例
  • 避免无意义的动态ElementId生成,保持组件实例的稳定性
  • 添加错误捕获,快速定位互调用失败原因

内容的提问来源于stack exchange,提问作者Killnine

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 23:02:14