| File: EventListenerCollection.cs | Web Access |
| Project: Microsoft.AspNetCore.Components.Platform.csproj (Microsoft.AspNetCore.Components.Platform) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components.Platform; /// <summary> /// Tracks the listeners added to one event target, so that scope disposal can remove them all. /// </summary> internal sealed partial class EventListenerCollection : IInternalAsyncDisposal { private readonly JSCallbackFactory _callbackFactory; private readonly ILogger _logger; private readonly List<IAsyncDisposable> _listeners = []; private bool _disposed; public EventListenerCollection(JSCallbackFactory callbackFactory, ILogger logger) { _callbackFactory = callbackFactory; _logger = logger; } /// <summary> /// Adds a listener for <paramref name="type"/> to <paramref name="target"/>. /// </summary> /// <returns>The handle that removes the listener when disposed.</returns> public async Task<IAsyncDisposable> AddAsync<[DynamicallyAccessedMembers(LinkerFlags.JsonSerialized)] TEvent>( IJSObjectReference target, string type, string shapesJson, Func<TEvent, Task> listener, CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_disposed, this); cancellationToken.ThrowIfCancellationRequested(); var eventListener = new EventListener<TEvent>(this, target, type, listener, cancellationToken, _logger); _listeners.Add(eventListener); try { await eventListener.AddAsync(_callbackFactory, shapesJson); } catch (Exception ex) { // The exception of the failed addition is the useful one. await TryDisposeAsync(eventListener); // Decided by the token, not by the error, as for other canceled browser operations. if (cancellationToken.IsCancellationRequested && ex is not OperationCanceledException) { throw new OperationCanceledException(ex.Message, ex, cancellationToken); } throw; } // The token can be canceled while addEventListener is on its way. if (cancellationToken.IsCancellationRequested) { await TryDisposeAsync(eventListener); cancellationToken.ThrowIfCancellationRequested(); } // Scope disposal can run while addEventListener is on its way, and it has already removed this listener. ObjectDisposedException.ThrowIf(_disposed, this); eventListener.RemoveWhenCanceled(); return eventListener; } internal void Remove(IAsyncDisposable listener) => _listeners.Remove(listener); async ValueTask IInternalAsyncDisposal.InternalDisposeAsync() { if (_disposed) { return; } _disposed = true; foreach (var listener in _listeners.ToArray()) { await TryDisposeAsync(listener); } } internal async ValueTask TryDisposeAsync(IAsyncDisposable listener) { try { await listener.DisposeAsync(); } catch (Exception ex) { Log.ListenerRemovalFailed(_logger, ex); } } internal static partial class Log { [LoggerMessage(2, LogLevel.Warning, "A listener could not be removed from its event target.", EventName = "ListenerRemovalFailed")] public static partial void ListenerRemovalFailed(ILogger logger, Exception exception); [LoggerMessage(1, LogLevel.Error, "A listener for the '{EventType}' event threw an exception.", EventName = "ListenerThrew")] public static partial void ListenerThrew(ILogger logger, string eventType, Exception exception); } }