How to await a storyboard, and other things
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:
- How to await a storyboard, and other things
- How to await a MediaElement
- How to await a drag operation
- How to await a button-click
- How to await a System.Diagnostics.Process (await an external program)
- How to write a custom awaiter
Other resources:
- AwaitableUI - a codeplex project that provides similar wrappers
- Awaitable ManagedAnimation class for WinRT
I want to be able to “await a storyboard”.
Await storyboard1.PlayAsync()
Actually, there are several syntaxes I might choose for awaiting a storyboard:
Await storyboard1
Await storyboard1.PlayAsync()
Await PlayStoryboardAsync(storyboard1)
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.
How to await a storyboard
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
tcs.TrySetResult(Nothing)
End Sub
AddHandler sb.Completed, lambda
sb.Begin()
Return tcs.Task
End Function
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:
<Page
x:Class="AppVB.MainPage"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:AppVB"
xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<Storyboard x:Name="storyboard1">
<DoubleAnimation Storyboard.TargetName="rectangle1"
Storyboard.TargetProperty="(Canvas.Left)"
From="0" To="500" Duration="0:0:1"/>
</Storyboard>
<MediaElement x:Name="mediaElement1" AutoPlay="False"
Source="Assets/boooo.mp3"/>
</Page.Resources>
<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"/>
</Canvas>
</Page>
Why make things awaitable?
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.
How to support the raw Await syntax
The first await syntax is the most striking:
Await e
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 tcs As New TaskCompletionSource(Of Object)
Dim lambda As EventHandler(Of Object) = Sub()
RemoveHandler sb.Completed, lambda
tcs.TrySetResult(Nothing)
End Sub
AddHandler sb.Completed, lambda
sb.Begin()
Dim t As Task = tcs.Task
Dim awaiter As System.Runtime.CompilerServices.TaskAwaiter = t.GetAwaiter()
Return awaiter
End Function
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:
<Extension> Function GetAwaiter(sb As Animation.Storyboard) As TaskAwaiter
Dim t As Task = SomeInternalFunctionAsync(...)
Dim awaiter As System.Runtime.CompilerServices.TaskAwaiter = t.GetAwaiter()
Return awaiter
End Function
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.
What syntax to choose for awaiting?
This article started with three possible syntaxes for awaiting:
Await storyboard1
Await storyboard1.PlayAsync()
Await PlayStoryboardAsync(storyboard1)
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. |
Comments
Anonymous
November 28, 2012
What about something like Await button1.WaitForClickAsync()? Similar to Task.Delay, the "action" you're starting is to do nothing for a while. Or maybe something like Await button1.WhenClicked(), similar to Task.WhenAny/WhenAll. You can obtain a Process object for processes you didn't start by doing things like Process.GetProcessById(), so I think it's reasonable to await a process directly. You can also use the static Process.Start methods so that you don't forget to call Start(), and Await Process.Start(fileName) feels fairly natural. You can actually make the same argument against directly awaiting a Task: if you use the Task constructor instead of Task.Run or Task.Factory.StartNew then you can forget to call Start.Anonymous
November 28, 2012
Call it button1.OnClickAsync()Anonymous
November 29, 2012
Hey, thanks for the suggestions. I like "await Button1.WhenClicked()" a lot. (yes, that way of creating "cold" tasks was a shame for async...)