Using TaskCompletionSource to await events
First of all, what is TaskCompletionSource<T>? It's a class that returns a task that does not finish immediately and then exposes methods such as TrySetResult. When the result is set, the task completes. We can use this class to turn an event based programming model to an await/async one.
In the example below I will use a Windows Forms app, just so I have access to the Click handler of a Button. Only instead of using the normal EventHandler approach, I will start a thread immediately after InitializeComponent that will react to button clicks.
Here is the Form constructor. Note that I am using Task.Factory.StartNew instead of Task.Run because I need to specify the TaskScheduler in order to have access to a TextBox object. If it were to log something or otherwise not involve the UI, a Task.Run would have been sufficient.
public Form1()
{
InitializeComponent();
Task.Factory.StartNew(async () =>
{
while (true)
{
await ClickAsync(button1);
textBox1.AppendText($"I was clicked at {DateTime.Now:HH:mm:ss.fffff}!\r\n");
}
},
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.FromCurrentSynchronizationContext());
}
What's going on here? I have a while (true) block and inside it I am awaiting a method then write something in a text box. Since await is smart enough to not use CPU and not block threads, this approach doesn't have any performance drawbacks.
Now, for the ClickAsync method:
private Task ClickAsync(Button button1)
{
var tcs = new TaskCompletionSource<object>();
void handler(object s, EventArgs e) => tcs.TrySetResult(null);
button1.Click += handler;
return tcs.Task.ContinueWith(_ => button1.Click -= handler);
}
Here I am creating a task completion source, I am adding a handler to the Click event, then I am returning the task, which I continue with removing the handler. The handler just sets the result on the task source, thus completing the task.
The flow comes as follows:
- the source is created
- the handler is attached
- the task is returned, but does not complete, thus the loop is halted in await
- when the button is clicked, the source result is set, then the handler is removed
- the task completed, the await finishes and the text is appended to the text box
- the loop continues
It would have been cool if the method to turn an event to an async method would have worked like this: await button1.Click.MakeAsync(), but events are not first class citizens in .NET. Instead, something more cumbersome can be used to make this more generic (note that there is no error handling, for demo purposes):
public Form1()
{
InitializeComponent();
Task.Factory.StartNew(async () =>
{
while (true)
{
await EventAsync(button1, nameof(Button.Click));
textBox1.AppendText($"I was clicked at {DateTime.Now:HH:mm:ss.fffff}!\r\n");
}
},
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.FromCurrentSynchronizationContext());
}
private Task EventAsync(object obj, string eventName)
{
var eventInfo = obj.GetType().GetEvent(eventName);
var tcs = new TaskCompletionSource<object>();
EventHandler handler = delegate (object s, EventArgs e) { tcs.TrySetResult(null); };
eventInfo.AddEventHandler(obj, handler);
return tcs.Task.ContinueWith(_ => eventInfo.RemoveEventHandler(obj, handler));
}
Notes:
- is this a better method of doing things? That depends on what you want to do.
- If you were to use Reactive Extensions, you can turn an event into an Observable with Observable.FromEventPattern.
- I see it useful not for button clicks (that while true loop scratches at my brain), but for classes that have Completed events.
- obviously the EventAsync method is not optimal and has no exception handling
Comments
Be the first to post a comment