A reusable pattern to convert event into task
.net, async-await, c#, events, generics
Solution
It is possible with a helper class and a fluent-like syntax:
public static class TaskExt
{
public static EAPTask<TEventArgs, EventHandler<TEventArgs>> FromEvent<TEventArgs>()
{
var tcs = new TaskCompletionSource<TEventArgs>();
var handler = new EventHandler<TEventArgs>((s, e) => tcs.TrySetResult(e));
return new EAPTask<TEventArgs, EventHandler<TEventArgs>>(tcs, handler);
}
}
public sealed class EAPTask<TEventArgs, TEventHandler>
where TEventHandler : class
{
private readonly TaskCompletionSource<TEventArgs> _completionSource;
private readonly TEventHandler _eventHandler;
public EAPTask(
TaskCompletionSource<TEventArgs> completionSource,
TEventHandler eventHandler)
{
_completionSource = completionSource;
_eventHandler = eventHandler;
}
public EAPTask<TEventArgs, TOtherEventHandler> WithHandlerConversion<TOtherEventHandler>(
Converter<TEventHandler, TOtherEventHandler> converter)
where TOtherEventHandler : class
{
return new EAPTask<TEventArgs, TOtherEventHandler>(
_completionSource, converter(_eventHandler));
}
public async Task<TEventArgs> Start(
Action<TEventHandler> subscribe,
Action action,
Action<TEventHandler> unsubscribe,
CancellationToken cancellationToken)
{
subscribe(_eventHandler);
try
{
using(cancellationToken.Register(() => _completionSource.SetCanceled()))
{
action();
return await _completionSource.Task;
}
}
finally
{
unsubscribe(_eventHandler);
}
}
}
Now you have a `WithHandlerConversion` helper method, which can infer type parameter from converter argument, which means you need to write `WebBrowserDocumentCompletedEventHandler` only one time. Usage:
await TaskExt
.FromEvent<WebBrowserDocumentCompletedEventArgs>()
.WithHandlerConversion(handler => new WebBrowserDocumentCompletedEventHandler(handler))
.Start(
handler => this.webBrowser.DocumentCompleted += handler,
() => this.webBrowser.Navigate(@"about:blank"),
handler => this.webBrowser.DocumentCompleted -= handler,
CancellationToken.None);
Problem
I'd like to have a generic reusable piece of code for wrapping EAP pattern as task, something similar to what `Task.Factory.FromAsync` does for `BeginXXX/EndXXX` APM pattern. E.g.: ``` private async void Form1_Load(object sender, EventArgs e) { await TaskExt.FromEvent<EventArgs>( handler => this.webBrowser.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(handler), () => this.webBrowser.Navigate("about:blank"), handler => this.webBrowser.DocumentCompleted -= new WebBrowserDocumentCompletedEventHandler(handler), CancellationToken.None); this.webBrowser.Document.InvokeScript("setTimeout", new[] { "document.body.style.backgroundColor = 'yellow'", "1" }); } ``` So far, it looks like this: ``` public static class TaskExt { public static async Task<TEventArgs> FromEvent<TEventArgs>( Action<EventHandler<TEventArgs>> registerEvent, Action action, Action<EventHandler<TEventArgs>> unregisterEvent, CancellationToken token) { var tcs = new TaskCompletionSource<TEventArgs>(); EventHandler<TEventArgs> handler = (sender, args) => tcs.TrySetResult(args); registerEvent(handler); try { using (token.Register(() => tcs.SetCanceled())) { action(); return await tcs.Task; } } finally { unregisterEvent(handler); } } } ``` Is it possible to come up with something similar, which nevertheless would not require me to type `WebBrowserDocumentCompletedEventHandler` twice (for `registerEvent`/`unregisterEvent`), without resorting to reflection?