scratch-link/scratch-link-common/EventAwaiter.cs
2023-06-01 14:52:19 -07:00

155 lines
7.2 KiB
C#

// <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!");
}
}
}