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

