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

Blazor中处理非DOM EventTarget事件的简便方法咨询

问题

在Blazor中,有没有内置或简便的方式处理来自EventTarget的非DOM事件?比如Web Permissions API里PermissionStatus的change事件——我能获取到作为EventTarget的PermissionStatus对象,但这个对象不在DOM树中,有没有更简单的方法处理它的change回调?

我现在的实现是通过JS互操作调用addEventListener,搭配带[JSInvokable]方法的处理包装器,自定义JS方法还要创建函数方便后续调用removeEventListener,单个事件就已经很复杂了。想知道我的实现是否正确?有没有我没了解到的内置或更优方案?

当前实现代码:

// Wrapper so JS can call it
internal class HandlerWrapper<T>
{
    Action<T> handler;
    T instance;
    public WebPermissionHandlerWrapper(Action<T> handler, T instance)
    {
        this.handler = handler;
        this.instance = instance;
    }

    [JSInvokable]
    public void InvokeHandler()
    {
        handler.Invoke(instance);
    }
}

// Add the listener
public async Task<IJSObjectReference> AddChangeEventHandlerAsync(Action<WebPermissionStatus> handler)
{
    var wrapper = new HandlerWrapper<WebPermissionStatus>(handler, this);
    var wrapperRef = DotNetObjectReference.Create(wrapper);

    var pointer = await js.InvokeAsync<IJSObjectReference>(
        "blazorInterop.addEventListener", jsRef, "change", wrapperRef);

    return pointer;
}
// JS interop function
addEventListener(obj, name, handlerWrapper) {
    const callback = async () => {
        await handlerWrapper.invokeMethodAsync("InvokeHandler");
    };

    obj.addEventListener(name, callback);

    return callback;
}

当前实现正确性分析

你的实现逻辑是正确的,核心满足了非DOM EventTarget的事件监听需求:

  • 通过DotNetObjectReference实现了JS与.NET之间的跨环境引用传递,确保回调能触发.NET端方法
  • 保存了事件回调的引用,为后续调用removeEventListener移除监听提供了可能
  • 包装类HandlerWrapper<T>完成了JS到.NET的调用桥接

但存在两处可优化细节:

  • 包装类构造函数名写错(WebPermissionHandlerWrapper应该对应类名HandlerWrapper)
  • 未处理DotNetObjectReference的释放,长期运行可能导致内存泄漏,建议在移除事件时调用wrapperRef.Dispose()

更优实现方案

方案1:封装通用事件监听工具类

可以打造一个通用工具类,统一处理非DOM EventTarget的事件监听逻辑,避免重复编写包装代码:

C# 通用工具类

public class EventTargetListener : IDisposable
{
    private readonly IJSObjectReference _jsModule;
    private readonly DotNetObjectReference<EventTargetListener> _selfRef;
    private IJSObjectReference? _callbackRef;
    private Action? _handler;

    public EventTargetListener(IJSObjectReference jsModule)
    {
        _jsModule = jsModule;
        _selfRef = DotNetObjectReference.Create(this);
    }

    public async Task SubscribeAsync(IJSObjectReference eventTarget, string eventName, Action handler)
    {
        _handler = handler;
        _callbackRef = await _jsModule.InvokeAsync<IJSObjectReference>(
            "addEventTargetListener", eventTarget, eventName, _selfRef);
    }

    [JSInvokable]
    public void OnEventTriggered()
    {
        _handler?.Invoke();
    }

    public async Task UnsubscribeAsync(IJSObjectReference eventTarget, string eventName)
    {
        if (_callbackRef != null)
        {
            await _jsModule.InvokeVoidAsync(
                "removeEventTargetListener", eventTarget, eventName, _callbackRef);
            await _callbackRef.DisposeAsync();
            _callbackRef = null;
        }
        _handler = null;
    }

    public void Dispose()
    {
        _selfRef.Dispose();
    }
}

对应的JS模块代码(event-target-utils.js)

export function addEventTargetListener(eventTarget, eventName, dotNetRef) {
    const callback = () => {
        dotNetRef.invokeMethodAsync('OnEventTriggered');
    };
    eventTarget.addEventListener(eventName, callback);
    return callback;
}

export function removeEventTargetListener(eventTarget, eventName, callback) {
    eventTarget.removeEventListener(eventName, callback);
}

使用示例

// 加载JS模块
var jsModule = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./event-target-utils.js");
var listener = new EventTargetListener(jsModule);

// 监听PermissionStatus的change事件(假设已获取到PermissionStatus的JS引用jsRef)
await listener.SubscribeAsync(jsRef, "change", () => {
    // 编写事件处理逻辑
});

// 移除监听时
await listener.UnsubscribeAsync(jsRef, "change");
listener.Dispose();

方案2:利用.NET 7+简化JS互操作

如果使用.NET 7及以上版本,可借助IJSInProcessObjectReference(仅同步场景适用)减少异步开销,还能直接传递.NET委托到JS,简化代码:

JS代码

export function addEventListenerDirect(eventTarget, eventName, dotNetDelegate) {
    const callback = () => {
        dotNetDelegate.invoke();
    };
    eventTarget.addEventListener(eventName, callback);
    return callback;
}

C#调用

var jsModule = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./utils.js");
Action handler = () => { /* 编写事件处理逻辑 */ };
var callbackRef = await jsModule.InvokeAsync<IJSObjectReference>(
    "addEventListenerDirect", jsRef, "change", handler);

注意:这种方式仍需及时移除监听并释放引用,避免内存泄漏。

方案3:结合Blazor组件封装(组件场景适用)

如果是在Blazor组件中使用,可以结合EventCallback封装,让事件处理更贴合Blazor组件开发习惯:

C# 组件类

public class PermissionStatusWatcher : ComponentBase, IDisposable
{
    [Inject] protected IJSRuntime JSRuntime { get; set; } = default!;
    [Parameter] public EventCallback<WebPermissionStatus> OnStatusChanged { get; set; }
    
    private IJSObjectReference? _jsModule;
    private DotNetObjectReference<PermissionStatusWatcher>? _selfRef;
    private IJSObjectReference? _callbackRef;
    private IJSObjectReference? _permissionStatusRef;

    protected override async Task OnInitializedAsync()
    {
        _jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./permission-watcher.js");
        _selfRef = DotNetObjectReference.Create(this);
        // 以通知权限为例,获取PermissionStatus对象
        _permissionStatusRef = await _jsModule.InvokeAsync<IJSObjectReference>("getPermissionStatus", "notifications");
        _callbackRef = await _jsModule.InvokeAsync<IJSObjectReference>(
            "subscribeToChange", _permissionStatusRef, _selfRef);
    }

    [JSInvokable]
    public async Task HandleStatusChange(WebPermissionStatus status)
    {
        await OnStatusChanged.InvokeAsync(status);
    }

    public async ValueTask DisposeAsync()
    {
        if (_callbackRef != null && _permissionStatusRef != null && _jsModule != null)
        {
            await _jsModule.InvokeVoidAsync("unsubscribeFromChange", _permissionStatusRef, "change", _callbackRef);
            await _callbackRef.DisposeAsync();
            await _permissionStatusRef.DisposeAsync();
            await _jsModule.DisposeAsync();
        }
        _selfRef?.Dispose();
    }
}

对应的JS模块(permission-watcher.js)

export async function getPermissionStatus(permissionName) {
    return await navigator.permissions.query({ name: permissionName });
}

export function subscribeToChange(permissionStatus, dotNetRef) {
    const callback = (event) => {
        dotNetRef.invokeMethodAsync('HandleStatusChange', event.target.state);
    };
    permissionStatus.addEventListener('change', callback);
    return callback;
}

export function unsubscribeFromChange(permissionStatus, eventName, callback) {
    permissionStatus.removeEventListener(eventName, callback);
}

组件使用示例

<PermissionStatusWatcher OnStatusChanged="HandlePermissionChange" />

@code {
    private void HandlePermissionChange(WebPermissionStatus status)
    {
        // 处理权限状态变化逻辑
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 16:48:08