// <copyright file="EventAwaiter.cs" company="Scratch Foundation"> // Copyright (c) Scratch Foundation. All rights reserved. // </copyright> namespace ScratchLink; using System; using System.Diagnostics; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; /// <summary> /// Adaptor to use "await" with events. /// Events which fire after construction but before calling <c>MakeTask</c> will be queued and can be retrieved later. /// </summary> /// <typeparam name="T">The type of EventArgs returned by this event.</typeparam> public class EventAwaiter<T> : IDisposable { private readonly Action<EventHandler<T>> removeHandler; private readonly Channel<T> events; private bool disposedValue; /// <summary> /// Initializes a new instance of the <see cref="EventAwaiter{T}"/> class. /// This version supports hooking an event in another class by providing <c>addHandler</c> and <c>removeHandler</c> actions. /// </summary> /// <param name="addHandler">An action to add a handler to the event, like <c>handler => foo.MyEvent += handler</c>.</param> /// <param name="removeHandler">An action to remove a handler from the event, like <c>handler => foo.MyEvent += handler</c>.</param> public EventAwaiter(Action<EventHandler<T>> addHandler, Action<EventHandler<T>> removeHandler) { this.removeHandler = removeHandler; this.events = Channel.CreateUnbounded<T>(); addHandler(this.EventHandler); } /// <summary> /// Convenience method to create an instance of the <see cref="EventAwaiter{T}"/> class, await its task, dispose of the instance, and return the event task's results. /// </summary> /// <param name="addHandler">An action to add a handler to the event, like <c>handler => foo.MyEvent += handler</c>.</param> /// <param name="removeHandler">An action to remove a handler from the event, like <c>handler => foo.MyEvent += handler</c>.</param> /// <param name="timeout">How long to wait for the event. If the timeout expires the Task will throw a <see cref="TimeoutException"/>.</param> /// <param name="cancellationToken">The cancellation token to use to cancel the operation.</param> /// <param name="action">Optional action to perform after hooking the event but before awaiting the event task. Usually this is code which will trigger the event.</param> /// <returns>A Task which will return the args passed to the event when it triggers.</returns> public static async ValueTask<T> MakeTask(Action<EventHandler<T>> addHandler, Action<EventHandler<T>> removeHandler, TimeSpan timeout, CancellationToken cancellationToken, Action action = null) { using (var awaiter = new EventAwaiter<T>(addHandler, removeHandler)) { action?.Invoke(); return await awaiter.MakeTask(timeout, cancellationToken); } } /// <summary> /// Convenience method to create an instance of the <see cref="EventAwaiter{T}"/> class, await its task, dispose of the instance, and return the event task's results. /// </summary> /// <param name="addHandler">An action to add a handler to the event, like <c>handler => foo.MyEvent += handler</c>.</param> /// <param name="removeHandler">An action to remove a handler from the event, like <c>handler => foo.MyEvent += handler</c>.</param> /// <param name="timeout">How long to wait for the event. If the timeout expires the Task will throw a <see cref="TimeoutException"/>.</param> /// <param name="cancellationToken">The cancellation token to use to cancel the operation.</param> /// <param name="asyncAction">Optional async action to await after hooking the event but before awaiting the event task. Usually this is code which will trigger the event.</param> /// <returns>A Task which will return the args passed to the event when it triggers.</returns> public static async ValueTask<T> MakeTask(Action<EventHandler<T>> addHandler, Action<EventHandler<T>> removeHandler, TimeSpan timeout, CancellationToken cancellationToken, Func<Task> asyncAction = null) { using (var awaiter = new EventAwaiter<T>(addHandler, removeHandler)) { if (asyncAction != null) { await asyncAction(); } return await awaiter.MakeTask(timeout, cancellationToken); } } /// <summary> /// Dispose of this <see cref="EventAwaiter{T}"/> instance, including ensuring that the event is no longer hooked. /// </summary> public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method this.Dispose(disposing: true); GC.SuppressFinalize(this); } /// <summary> /// Make an awaitable Task which will resolve the next time this event is triggered. /// See <a href="https://stackoverflow.com/q/2560258">here</a> for more information and alternative solutions. /// </summary> /// <param name="timeout">How long to wait for the event. If the timeout expires the Task will throw a <see cref="TimeoutException"/>.</param> /// <param name="cancellationToken">The cancellation token to use to cancel the operation.</param> /// <returns>A Task which will return the args passed to the event when it triggers.</returns> public async ValueTask<T> MakeTask(TimeSpan timeout, CancellationToken cancellationToken) { // timeoutCancellationSource should only affect the task results on timeout, not when it is disposed. // If cancellationToken is cancelled it happed externally and should mark the task as canceled. using var timeoutCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCancellationSource.CancelAfter(timeout); var localCancellationToken = timeoutCancellationSource.Token; T result; try { result = await this.events.Reader.ReadAsync(localCancellationToken); } catch (OperationCanceledException e) { if (timeoutCancellationSource.IsCancellationRequested && !cancellationToken.IsCancellationRequested) { throw new TimeoutException(); } throw e; } return result; } /// <summary> /// Implements the Dispose pattern for <see cref="EventAwaiter{T}"/>. /// </summary> /// <param name="disposing">True if called from <see cref="Dispose()"/>.</param> protected virtual void Dispose(bool disposing) { if (!this.disposedValue) { try { if (disposing) { // TODO: dispose managed state (managed objects) this.removeHandler(this.EventHandler); } } finally { // TODO: free unmanaged resources (unmanaged objects) and override finalizer // TODO: set large fields to null this.disposedValue = true; } } } private void EventHandler(object sender, T args) { if (!this.events.Writer.TryWrite(args)) { Trace.WriteLine("EventAwaiter failed to write an event!"); } } }