I think the following version might be satisfactory enough. I did borrow the idea of preparing a correctly typed event handler from max's answer, but this implementation doesn't create any additional object explicitly.
As a positive side effect, it allows the caller to cancel or reject the result of the operation (with an exception), based upon the event's arguments (like AsyncCompletedEventArgs.Cancelled
, AsyncCompletedEventArgs.Error
).
The underlying TaskCompletionSource
is still completely hidden from the caller (so it could be replaced with something else, e.g. a custom awaiter or a custom promise):
private async void Form1_Load(object sender, EventArgs e)
{
await TaskExt.FromEvent<WebBrowserDocumentCompletedEventHandler, EventArgs>(
getHandler: (completeAction, cancelAction, rejectAction) =>
(eventSource, eventArgs) => completeAction(eventArgs),
subscribe: eventHandler =>
this.webBrowser.DocumentCompleted += eventHandler,
unsubscribe: eventHandler =>
this.webBrowser.DocumentCompleted -= eventHandler,
initiate: (completeAction, cancelAction, rejectAction) =>
this.webBrowser.Navigate("about:blank"),
token: CancellationToken.None);
this.webBrowser.Document.InvokeScript("setTimeout",
new[] { "document.body.style.backgroundColor = 'yellow'", "1" });
}
public static class TaskExt
{
public static async Task<TEventArgs> FromEvent<TEventHandler, TEventArgs>(
Func<Action<TEventArgs>, Action, Action<Exception>, TEventHandler> getHandler,
Action<TEventHandler> subscribe,
Action<TEventHandler> unsubscribe,
Action<Action<TEventArgs>, Action, Action<Exception>> initiate,
CancellationToken token = default) where TEventHandler : Delegate
{
var tcs = new TaskCompletionSource<TEventArgs>();
Action<TEventArgs> complete = args => tcs.TrySetResult(args);
Action cancel = () => tcs.TrySetCanceled();
Action<Exception> reject = ex => tcs.TrySetException(ex);
TEventHandler handler = getHandler(complete, cancel, reject);
subscribe(handler);
try
{
using (token.Register(() => tcs.TrySetCanceled(),
useSynchronizationContext: false))
{
initiate(complete, cancel, reject);
return await tcs.Task;
}
}
finally
{
unsubscribe(handler);
}
}
}
This actually can be used to await any callback, not just event handlers, e.g.:
var mre = new ManualResetEvent(false);
RegisteredWaitHandle rwh = null;
await TaskExt.FromEvent<WaitOrTimerCallback, bool>(
(complete, cancel, reject) =>
(state, timeout) => { if (!timeout) complete(true); else cancel(); },
callback =>
rwh = ThreadPool.RegisterWaitForSingleObject(mre, callback, null, 1000, true),
callback =>
rwh.Unregister(mre),
(complete, cancel, reject) =>
ThreadPool.QueueUserWorkItem(state => { Thread.Sleep(500); mre.Set(); }),
CancellationToken.None);
Updated, less boilerplate for a simple event case (I use this one more often these days):
public static async Task<TEventArgs> FromEvent<TEventHandler, TEventArgs>(
Action<TEventHandler> subscribe,
Action<TEventHandler> unsubscribe,
CancellationToken token = default,
bool runContinuationsAsynchronously = true)
where TEventHandler : Delegate
where TEventArgs: EventArgs
{
var tcs = new TaskCompletionSource<TEventArgs>(runContinuationsAsynchronously ?
TaskCreationOptions.RunContinuationsAsynchronously :
TaskCreationOptions.None);
var handler = new Action<object?, TEventArgs>((_, args) => tcs.TrySetResult(args));
var h = (TEventHandler)Delegate.CreateDelegate(typeof(TEventHandler), handler.Target, handler.Method);
subscribe(h);
try
{
using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false))
{
return await tcs.Task;
}
}
finally
{
unsubscribe(h);
}
}
Usage:
await TaskExt.FromEvent<FormClosedEventHandler, FormClosedEventArgs>(
h => mainForm.FormClosed += h,
h => mainForm.FormClosed -= h,
token);