开发者

Using Async CTP Await with events?

Relevant to Silverlight 5 / Async CTP

I want to create an asynchronous function that initiates a layout update and then Awaits for the layout update to complete. Something like:

    Private Async Function UpdateLayoutRoot() As Task
       LayoutRoot.UpdateLayout()
     开发者_StackOverflow  Await LayoutRoot.LayoutUpdated  <--- (NOT valid but shows desired outcome)           
    End Function

How can this be done? More generally, how can you use Await to wait for existing events?


One way to accomplish this is to await on a TaskCompletionSource that is set inside the event. I don't know VB.NET, hopefully you can understand it from C#:

// The type and value returned by the TaskCompletionSource
// doesn't matter, so I just picked int.
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();

// The delegate sets the TaskCompletionSource -- the result value
// doesn't matter, we only care about setting it.  Keep hold of
// the delegate so it can be removed later.
EventHandler d = (o, e) => { tcs.TrySetResult(1); };

LayoutRoot.LayoutUpdate += d;

try
{
    LayoutRoot.UpdateLayout();
    await tcs.Task;
}
finally
{
    // Don't leak the delegate!
    LayoutRoot.LayoutUpdate -= d;
}


Thanks Cory! Your suggestion to use TaskCompletionSource is just what I needed. I've combined the use of a TaskCompletionSource with the Lucian Wischik's Async CTP specification to develop a pair of generic Silverlight 5 classes that can be used to Await any CLR, or routed event. Only the Async CTP (AsyncCtpLibrary_Silverlight) is required (The formidable Rx library is not needed). Here are the two classes:

Public Class AwaitableEvent(Of TResult)

    Private eta As EventTaskAwaiter(Of TResult) = Nothing

    Sub New(ByVal Sender As Object, ByVal EventName As String)
        eta = New EventTaskAwaiter(Of TResult)
        Dim ei as EventInfo = Sender.GetType.GetEvent(EventName)
        Dim d = [Delegate].CreateDelegate(ei.EventHandlerType, 
            Me, "EventCompletedHandler", True, True)
        ei.AddEventHandler(Sender, d)
    End Sub

    Public Function GetAwaiter() As EventTaskAwaiter(Of TResult)
        Return eta
    End Function

    Private Sub EventCompletedHandler(ByVal sender As Object, ByVal e As TResult)
        eta.tcs.TrySetResult(e)
    End Sub

End Class

Public Class EventTaskAwaiter(Of TResult)

    Friend tcs As New TaskCompletionSource(Of TResult)

    Public ReadOnly Property IsCompleted As Boolean
        Get
            Return tcs.Task.IsCompleted
        End Get
    End Property

    Sub OnCompleted(r As Action)
        Dim sc = SynchronizationContext.Current
        If sc Is Nothing Then
            tcs.Task.ContinueWith(Sub() r())
        Else
            tcs.Task.ContinueWith(Sub() sc.Post(Sub() r(), Nothing))
        End If
    End Sub

    Function GetResult() As TResult
        If tcs.Task.IsCanceled Then Throw New TaskCanceledException(tcs.Task)
        If tcs.Task.IsFaulted Then Throw tcs.Task.Exception.InnerException
        Return tcs.Task.Result
    End Function

End Class

Here's an example of using the AwaitableEvent class to Await mouse, keyboard and timer events.

Private Sub AECaller()
    GetMouseButtonAsync()
    MessageBox.Show("After Await mouse button event")
    GetKeyAsync()
    MessageBox.Show("After Await key event")
    GetTimerAsync()
    MessageBox.Show("After Await timer")
End Sub

Private Async Sub GetMouseButtonAsync()
    Dim ae As New AwaitableEvent(Of MouseButtonEventArgs)(LayoutRoot, "MouseLeftButtonDown")
    Dim e = Await ae
    MessageBox.Show(String.Format("Clicked {0} at {1},{2}",
                                  e.OriginalSource.ToString,
                                  e.GetPosition(LayoutRoot).X,
                                  e.GetPosition(LayoutRoot).Y))
End Sub

Private Async Sub GetKeyAsync()
    Dim ae As New AwaitableEvent(Of KeyEventArgs)(LayoutRoot, "KeyDown")
    Dim e = Await ae
    MessageBox.Show(String.Format("Key {0} was pressed", e.Key.ToString))
End Sub

Private Async Sub GetTimerAsync()
    Dim StopWatch As New DispatcherTimer
    StopWatch.Interval = New TimeSpan(TimeSpan.TicksPerSecond * 6)
    Dim ae As New AwaitableEvent(Of EventArgs)(StopWatch, "Tick")
    StopWatch.Start()
    Await ae
    MessageBox.Show(String.Format("It's {0}seconds later!", StopWatch.Interval.TotalSeconds))
    StopWatch.Stop()
End Sub

As expected the Await statement returns control to the calling function immediately. When the events are subsequently completed, Await assigns the result (the event args appropriate for the event being monitored) and the remaining code in the asynchronous method is then run.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜