This post is part of a series about an important new design pattern, awaiting events... I also made a Channel9 video introduction "Async Over Events". In this blog series:
I want to be able to “await a storyboard”.
Actually, there are several syntaxes I might choose for awaiting a storyboard:
Indeed there are lots of other things that I’d like to be awaitable but aren’t (and for each of them I’ll have to choose which of the three syntaxes). This article is about making things become awaitable. First we’ll see how to make things awaitable. Then we’ll evaluate which of the three syntaxes is appropriate in which circumstances.
I’ll jump straight to the punch-line. This is the code you need to await a storyboard in win8:
<Extension> Function PlayAsync(sb As Animation.Storyboard) As Task
Dim tcs As New TaskCompletionSource(Of Object)
Dim lambda As EventHandler(Of Object) = Sub()
RemoveHandler sb.Completed, lambda
AddHandler sb.Completed, lambda
For this example, and for the next few articles, I’m working on a simple “blank page” Windows 8 app with the following in its Mainpage.xaml:
From="0" To="500" Duration="0:0:1"/>
<MediaElement x:Name="mediaElement1" AutoPlay="False" Source="Assets/boooo.mp3"/>
<Canvas x:Name="canvas1" Background="Black">
<Button x:Name="button1" Content="Button" Canvas.Top="150"/>
<Rectangle x:Name="rectangle1" Fill="yellow" Width="100" Height="100"/>
There are three main reasons for wanting something to be awaitable:
(1) If I can await the storyboard, then I can use the “await” keyword instead of adding a callback handler to the Storyboard.Completed event. I don’t like callbacks because they turn into spaghetti code; I prefer to use “await” because it keeps my code readable.
(2) If I can await the storyboard via the last two syntaxes, then it opens up powerful possibilities, e.g.
Await Task.WhenAll(storyboard1.PlayAsync(), mediaElement1.PlayAsync())
In a future article I’ll dive into the new async choreography design patterns that this opens up, for coding the sequences of button-clicks and animations and events that make up an interactive application.
(3) If I make something awaitable, then it gives me some interesting flexibility. I can change the way that code resumes after an await, similar to how “Await t.ConfigureAwait(false)” resumes more efficiently. I can make an awaitable that resumes at a different priority or even a different thread, e.g. “Await SwitchThread()”. I can implement co-routines. These advanced techniques all require custom awaiters, which I’ll address in a future article.
The first await syntax is the most striking:
If the compiler encounters the above expression, then under the hood it makes a call to
Dim temp = e.GetAwaiter()
Therefore: to allow users to write “Await e”, we must provide a GetAwaiter method. It has to return a value which satisfies the awaiter pattern. I’ll spell out the full details of that in a future post, but for now we’ll just return an instance of System.Runtime.CompilerServices.TaskAwaiter, or its generic version TaskAwaiter(Of T). Here’s how I might do it for Storyboard. Storyboard is a built-in type, so I can’t add GetAwaiter as an instance method, so instead I’ll provide it as an extension method:
<Extension> Function GetAwaiter(sb As Animation.Storyboard) As TaskAwaiter
Dim t As Task = tcs.Task
Dim awaiter As System.Runtime.CompilerServices.TaskAwaiter = t.GetAwaiter()
NB. The first code in this function was for “<Extension> Function PlayAsync(...) As Task”. The reason that works is because the built-in Task type has its own GetAwaiter() instance method which returns TaskAwaiter.
If I want to allow users to write “Dim x As Integer = Await storyboard1”, then I’d have had to return TaskAwaiter(Of Integer) instead. And likewise, Task(Of T) has a GetAwaiter() instance method which returns TaskAwaiter(Of T).
There are some additional restrictions. GetAwaiter must be a method (not a property or delegate field). It cannot have any optional parameters. It’s allowed to be an instance or an extension method, and if an extension method then it’s allowed to be generic. (Technical note: if the call to GetAwaiter were late-bound, i.e. if “e” had type Object in VB or dynamic in C#, then the restrictions are relaxed and normal late-bound rules apply.)
You might be wondering about callbacks. The whole point of “await” is to liberate you from callbacks because they’re unpleasant to code with. But we’ve used a callback right here in the GetAwaiter method. Well, what we’ve done is localize the callbacks. The entire rest of my code can happily use “Await Storyboard” without ever using callbacks. It’s only this one small localized routine that encapsulates the callback.
Note: I’m doing this trick with “TaskCompletionSource” because I’m awaiting something that fires events. TaskCompletionSource is the way to turn an event-based API into a Task-based API. (It’s my experience that most things I want to await, if they aren’t already awaitable, are based around events.) If I were awaiting something easier, I might have been able to skip TaskCompletionSource entirely and instead just done this:
Dim t As Task = SomeInternalFunctionAsync(...)
Note: There doesn’t exist a plain non-generic “TaskCompletionSource” for creating a non-generic Task. The only thing you can do is use the generic “TaskCompletionSource(Of T)”, create a generic “Task(Of T)”, and then cast the result to the non-generic Task.
This article started with three possible syntaxes for awaiting:
It’s up to us as coders which syntax we’ll support. The first option looks slick, but I think it is poor style – because it’s not clear what is being awaited. The next two options are equally fine.
Lucian’s personal recommendation: I think it is good style to await an imperative verb (method) that ends in the suffix “Async”. It is good style to await a noun (expression) so long as the type of that noun has the characteristics of a hot task, and not much more, and it’s obvious which completion event you’re thinking about.
Here are some examples of how I might chose to make something awaitable.
Dim iaa As IAsyncAction = ...
Await iaa ' good
Await iaa.StartAsync() ' bad
Await iaa.AsTask() ' good
Windows.Foundation.IAsyncAction is always given to you “hot” i.e. already running. Also it really is very much like a task, i.e. it starts then finishes then can’t be restarted. Its only additional behaviors are cancellation and progress, which are also task-like. Therefore “Await iaa” is easily understandable. The form “Await iaa.StartAsync()” is confusing because it has already started.
Dim timeout_ms = 150
Await timeout_ms ' bad
Await Task.Delay(timeout_ms) ' good
The first form is just confusing. There’s no obvious completion event for a number!
Dim sb As StoryBoard = ...
Await sb ' bad
Await sb.PlayAsync() ' good
Windows.UI.Xaml.Media.Animation.Storyboard feels like a very rich type, with far more properties and behaviors than just a Task-like thing, so it’s good to call out explicitly which of those behaviors we’re awaiting. And again, the second form makes it clear that we’re encapsulating a call to sb.Start().
Dim media As MediaElement = ...
Await media ' bad
Await media.PlayAsync() ' good
Windows.UI.Xaml.Controls.MediaElement has a common well-understood verb, “Play”. When we make it awaitable, of course we should it clear that we’re awaiting completion of that verb.
Dim r As Rectangle = ...
Await r ' bad
Await r.DragAsync() ' bad Await DragAsync(r) ' good
Windows.UI.Xaml.Shapes.Rectangle. Here I want to await until a drag operation has finished. The first form is terrible because it’s not clear what’s being awaited. The second form is ugly because DragAsync() isn’t an inherent property of shapes, and should be an extension method on them.
Await button1 ' ?
Await button1.ClickAsync() ' ?
Await ButtonClickAsync(button1) ' ?
Await ButtonClick(button1) ' ?
What do you think of the first form? It’s fairly obvious that we’re waiting until the button gets clicked. The second form I think looks bad because it gives the impression we’re performing the click. I’m undecided between the third and fourth forms.
Dim proc As Process = ...
Await proc ' ?
Await proc.RunAsync() ' good
System.Diagnostics.Process is often thought of like a task, i.e. something with a well-defined lifetime. So I wouldn’t mind awaiting it directly. However, users might easily forget to call Start() on it and so I prefer the second form.