mirror of
https://github.com/scratchfoundation/scratch-link.git
synced 2025-07-10 20:44:04 -04:00
155 lines
7.2 KiB
C#
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!");
|
|
}
|
|
}
|
|
}
|