2023-06-01 14:46:23 -07:00
// <copyright file="EventAwaiter.cs" company="Scratch Foundation">
2022-05-17 12:37:19 -07:00
// Copyright (c) Scratch Foundation. All rights reserved.
// </copyright>
namespace ScratchLink ;
using System ;
2022-05-26 15:00:56 -07:00
using System.Diagnostics ;
2022-07-07 17:40:49 -07:00
using System.Threading ;
2022-05-26 15:00:56 -07:00
using System.Threading.Channels ;
2022-07-07 17:40:49 -07:00
using System.Threading.Tasks ;
2022-05-17 12:37:19 -07:00
/// <summary>
/// Adaptor to use "await" with events.
2022-07-18 16:36:33 -07:00
/// Events which fire after construction but before calling <c>MakeTask</c> will be queued and can be retrieved later.
2022-05-17 12:37:19 -07:00
/// </summary>
2022-05-26 15:00:56 -07:00
/// <typeparam name="T">The type of EventArgs returned by this event.</typeparam>
public class EventAwaiter < T > : IDisposable
2022-05-17 12:37:19 -07:00
{
2022-05-26 15:00:56 -07:00
private readonly Action < EventHandler < T > > removeHandler ;
private readonly Channel < T > events ;
private bool disposedValue ;
2022-05-17 12:37:19 -07:00
/// <summary>
2022-05-26 15:00:56 -07:00
/// Initializes a new instance of the <see cref="EventAwaiter{T}"/> class.
2022-05-17 12:37:19 -07:00
/// 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>
2022-05-26 15:00:56 -07:00
public EventAwaiter ( Action < EventHandler < T > > addHandler , Action < EventHandler < T > > removeHandler )
{
this . removeHandler = removeHandler ;
this . events = Channel . CreateUnbounded < T > ( ) ;
addHandler ( this . EventHandler ) ;
}
2022-07-18 16:36:33 -07:00
/// <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 ) )
{
2023-06-01 14:46:23 -07:00
action ? . Invoke ( ) ;
2022-07-18 16:36:33 -07:00
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 ) ;
}
}
2022-05-26 15:00:56 -07:00
/// <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>
2022-05-17 12:37:19 -07:00
/// <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>
2022-08-01 16:39:20 -07:00
public async ValueTask < T > MakeTask ( TimeSpan timeout , CancellationToken cancellationToken )
2022-05-17 12:37:19 -07:00
{
2022-08-01 16:39:20 -07:00
// timeoutCancellationSource should only affect the task results on timeout, not when it is disposed.
2022-05-18 14:33:33 -07:00
// If cancellationToken is cancelled it happed externally and should mark the task as canceled.
2022-08-01 16:39:20 -07:00
using var timeoutCancellationSource = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
timeoutCancellationSource . CancelAfter ( timeout ) ;
2022-05-26 15:00:56 -07:00
2022-08-01 16:39:20 -07:00
var localCancellationToken = timeoutCancellationSource . Token ;
2022-05-18 14:33:33 -07:00
2022-08-01 16:39:20 -07:00
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 ;
2022-05-26 15:00:56 -07:00
}
2022-05-17 12:37:19 -07:00
2022-05-26 15:00:56 -07:00
/// <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 )
2022-05-17 12:37:19 -07:00
{
2022-05-26 15:00:56 -07:00
try
2022-05-17 12:37:19 -07:00
{
2022-05-26 15:00:56 -07:00
if ( disposing )
{
// TODO: dispose managed state (managed objects)
this . removeHandler ( this . EventHandler ) ;
}
2022-05-17 12:37:19 -07:00
}
2022-05-26 15:00:56 -07:00
finally
2022-05-17 12:37:19 -07:00
{
2022-05-26 15:00:56 -07:00
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
// TODO: set large fields to null
this . disposedValue = true ;
2022-05-17 12:37:19 -07:00
}
2022-05-26 15:00:56 -07:00
}
}
2022-05-17 12:37:19 -07:00
2022-05-26 15:00:56 -07:00
private void EventHandler ( object sender , T args )
{
if ( ! this . events . Writer . TryWrite ( args ) )
2022-05-17 12:37:19 -07:00
{
2022-09-09 17:31:06 -07:00
Trace . WriteLine ( "EventAwaiter failed to write an event!" ) ;
2022-05-26 15:00:56 -07:00
}
2022-05-17 12:37:19 -07:00
}
}