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()
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.
Related
I'm trying to create a bot that place multiples orders on Binance and I need a fast coin price parsing.
I'm parsing the code using this code
Dim price as decimal
Private Async Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
Timer1.Stop()
Dim downloadTasks As New List(Of Task(Of String))
Dim dogebusd = wc1.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/price?symbol=DOGEBUSD")
downloadTasks.Add(dogebusd)
Await Task.WhenAll(downloadTasks)
Dim d = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(dogebusd.Result)
Dim PREZZO As Decimal = CDec(Val((d("price")).ToString))
price = CDec((PREZZO))
Timer1.Start()
End Sub
but when the price is dumping or pumping really fast, even a 10 ms or 100ms timer tick is not that efficient.
I wonder if this is the fastest way possible or if I can improve the code.
Thanks
following Jimi suggestions:
Dim price As Decimal
Private ReadOnly _timer As New PeriodicTimer(TimeSpan.FromSeconds(1))
Private Async Sub Timer1_Tick(sender As Object, e As EventArgs) Handles _timer.Tick
timer1.stop
Dim dogebusdTask = HttpClient.GetStringAsync("https://api.binance.com/api/v1/ticker/price?symbol=DOGEBUSD")
Await dogebusdTask
Dim d = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(dogebusdTask.Result)
If Decimal.TryParse(d("price"), NumberStyles.Number, CultureInfo.InvariantCulture, price) Then
_timer.Start()
End If
End Sub
An example, using the .NET 6+ awaitable PeriodicTimer and System.Text.Json to parse the resulting stream returned by HttpClient's GetStreamAsync()
The PeriodicTimer can be quite efficient in contexts like this, since it tries to keep up with the interval you have provided. Since the internal procedure takes time to execute - the HttpClient.GetStreamAsync() takes an undetermined amount of time to execute and return a result - the Timer keeps track of the actual elapsed time and tries to keep ticking steadily (unless the procedure takes more than the specified Interval, then you skip a beat, of course)
Note: as usual, when dealing with Tasks that accept a CancellationToken, you should run this code without a Debugger (CTRL+F5) or configure the Debugger to not halt on TaskCanceledException or OperationCanceledException, otherwise it handles the (ignorable) exception before you do
Imports System.Globalization
Imports System.Net.Http
Imports System.Text.Json
Imports System.Threading
Private Shared ReadOnly m_Client As New HttpClient()
Private timerCts As CancellationTokenSource = Nothing
Private m_PeriodicTimer As PeriodicTimer = Nothing
Private m_PeriodicTimerInterval As Integer = 500
Private currentPrice As Decimal = 0.0D
Private Async Function StartPriceLookupTimer(token As CancellationToken, timerInterval As Integer) As Task
If token.IsCancellationRequested Then Return
Dim lookupAddress = "https://api.binance.com/api/v1/ticker/price?symbol=DOGEBUSD"
m_PeriodicTimer = New PeriodicTimer(TimeSpan.FromMilliseconds(timerInterval))
Try
While Await m_PeriodicTimer.WaitForNextTickAsync(token)
Dim jsonStream = Await m_Client.GetStreamAsync(lookupAddress, token)
Try
Dim prop = (Await JsonDocument.ParseAsync(jsonStream, Nothing, token)).RootElement.GetProperty("price")
Dim strPrice = prop.GetString()
If Decimal.TryParse(strPrice, CultureInfo.InvariantCulture, currentPrice) Then
' Do whatever you need to do with the parsed value
' E.g., assign it to a TextBox for presentation,
' using the current Culture format, since strPrice contains the original format
[Some TextBox].Text = currentPrice.ToString() ' <= UI Thread here
End If
Catch knfex As KeyNotFoundException
Debug.WriteLine("The JSON property was not found")
Catch tcex As TaskCanceledException
Debug.WriteLine("The lookup procedure was canceled")
End Try
End While
Catch tcex As TaskCanceledException
Debug.WriteLine("The lookup procedure was canceled")
End Try
End Function
Private Sub StopPriceLookupTimer()
timerCts?.Cancel()
m_PeriodicTimer?.Dispose()
timerCts?.Dispose()
timerCts = Nothing
End Sub
How to start this lookup procedure, calling the StartPriceLookupTimer() method?
You could use a Button, making its Click handler async:
Private Async Sub SomeButton_Click(sender As Object, e As EventArgs) Handles SomeButton.Click
If timerCts Is Nothing Then
timerCts = New CancellationTokenSource()
Await StartPriceLookupTimer(timerCts.Token, m_PeriodicTimerInterval)
Else
Debug.WriteLine("Lookup Timer already started")
End If
End Sub
Or make the Form's Shown Handler / OnShown() method override async and start the procedure when the Form is first presented:
Protected Overrides Async Sub OnShown(e As EventArgs)
MyBase.OnShown(e)
timerCts = New CancellationTokenSource()
Await StartPriceLookupTimer(timerCts.Token, m_PeriodicTimerInterval)
End Sub
To stop the lookup procedure, call the StopPriceLookupTimer() method when needed, e.g., using another Button or in the Form.Closed handler / OnFormClosed() override
I'm a beginner using async in VB.NET. I read online help but some things aren't clear.
I try to use tweetinvi library
I got this:
Namespace tweet_invi
Class twitter_call
Public Shared Async Function twitter_get_user_info_from_id(id As Long) As Task
Dim userClient = New TwitterClient(ConfigurationManager.AppSettings("consumerKey"), ConfigurationManager.AppSettings("consumerSecret"), ConfigurationManager.AppSettings("accessToken"), ConfigurationManager.AppSettings("accessTokenSecret"))
Dim tweetinviUser = Await userClient.Users.GetUserAsync(id)
Dim description As String = tweetinviUser.Description
End Function
End Class
End Namespace
And the module from where i would launch this async function
Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
Dim toto As Long = 1311275527223812096
Dim result = tweet_invi.twitter_call.twitter_get_user_info_from_id(toto)
End Sub
My issue: result is a task. How do i have to get the value of description?
You can see it in the code you posted. The second line of that method does it. You use the Await operator to await the completion of the Task.
That said, there is no result to get anyway. If you have a synchronous Sub then that becomes an asynchronous Function that returns a Task. In both cases, there is no actual value to get out of the method. As such, awaiting such a method doesn't return anything. If you have a synchronous Function with a return type of T then that becomes an asynchronous Function that returns a Task(Of T). Awaiting that gives you a result of type T.
If you had these methods:
Private Sub DoSomething()
'...
End Sub
Private Function GetSomething() As SomeType
'...
End Function
then you'd call them like this:
DoSomething()
Dim someValue As SomeType = GetSomething()
If you had these methods:
Private Async Function DoSomethingAsync() As Task
'...
End Function
Private Async Function GetSomethingAsync() As Task(Of SomeType)
'...
End Function
then you'd call them like this:
Await DoSomethingAsync()
Dim someValue As SomeType = Await GetSomethingAsync()
VB actually does support Async Sub but the ONLY time you should ever us it is for event handlers, which MUST be declared Sub, i.e. you cannot handle an event with a Function. Also, any method in which you want to use the Await operator must be declared Async. Together, that means that you must declare the Click event handler of your Button as Async Sub and then you can await an asynchronous method in it:
Private Async Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
Dim toto As Long = 1311275527223812096
Await tweet_invi.twitter_call.twitter_get_user_info_from_id(toto)
End Sub
With regards to the code you posted, that twitter_get_user_info_from_id method is useless. It declares and sets some local variables but does nothing with the data it gets. I suspect that that method should be like this:
Namespace tweet_invi
Class twitter_call
Public Shared Async Function twitter_get_user_info_from_id(id As Long) As Task(Of String)
Dim userClient = New TwitterClient(ConfigurationManager.AppSettings("consumerKey"), ConfigurationManager.AppSettings("consumerSecret"), ConfigurationManager.AppSettings("accessToken"), ConfigurationManager.AppSettings("accessTokenSecret"))
Dim tweetinviUser = Await userClient.Users.GetUserAsync(id)
Dim description As String = tweetinviUser.Description
Return description
End Function
End Class
End Namespace
and then you would call it like this:
Private Async Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
Dim toto As Long = 1311275527223812096
Dim userInfo = Await tweet_invi.twitter_call.twitter_get_user_info_from_id(toto)
'...
End Sub
I am using this snippet to validate my NeverBounce account:
Private Sub btnGo_Click(sender As Object, e As RoutedEventArgs) Handles btnGo.Click
Dim nb As NeverBounceSdk = New NeverBounceSdk("secret_.......")
Dim rsp As AccountInfoResponseModel = nb.Account.Info().Result
txtRes.Text = rsp.ToString
End Sub
But I do not get a response. Based on the NeverBounce documentation, this appears to be the correct approach. Do you know why this doesn't work?
Async methods must be used. See the following.
Public Async Function IsValidAsync() As Task(Of Boolean)
Dim val As SingleResponseModel = Await ValidateAddressAsync()
Return If(val.result = "valid", True, False)
End Function
Private Async Function ValidateAddressAsync() As Task(Of SingleResponseModel)
Dim nb As NeverBounceSdk = New NeverBounceSdk("NeverBounce key")
Dim mdl As New SingleRequestModel With {.email = FullAddress, .timeout = 30}
Return Await nb.Single.Check(mdl)
End Function
I am trying to update the progress bar which is on my Form1 using Form1.ProgressBar1.PerformStep() from a class function. I am using the async functions. There are 10 processes running but the progress bar should be updated based on the value of the records read (and later processed) within the While loop. It looks like I'm not able to update the GUI using the code below. I have tried begininvoke and invoke but not luck. Any ideas?
Await Cheque.MultiProcessCheques()
Public Shared Async Function MultiProcessCheques() As Task
Dim tasks As New List(Of Task)()
For i As Integer = 0 To 9
Dim temp_i As Integer = i
tasks.Add(Task.Run(Function() Cheque.CopyBinaryValueToFile(temp_i)))
Next
Await Task.WhenAll(tasks)
End Function
Public Shared Async Function CopyBinaryValueToFile(i As Integer) As Task
Try
Using connection = ConnectionController.GetConnection
Await connection.OpenAsync()
Using command = ConnectionController.GetCommand
command.CommandText = ("SELECT ch.RECORDID FROM TABLE WHERE VALUE = '%" & i & "'")
command.Connection = connection
command.CommandTimeout = 0
Using reader As Common.DbDataReader = Await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess)
While Await reader.ReadAsync()
If reader.HasRows Then
End If
' update Progress bar here
Form1.ProgressBar1.PerformStep()
End While
End Using
End Using
End Using
Catch ex As Exception
MessageBox.Show("1" & ex.ToString)
End Try
End Function
End Class
Create a Shared Member to hold the Form1 reference in your Cheque Class, and modify your MultiProcessCheques() function to receive a reference:
Public Class Cheque
Private Shared F1 As Form1
Public Shared Async Function MultiProcessCheques(ByVal f1 As Form1) As Task
Cheque.F1 = f1
' ... other code ...
End Function
Public Shared Async Function CopyBinaryValueToFile(i As Integer) As Task
' ... other code ...
Cheque.F1.Invoke(Sub()
Cheque.F1.ProgressBar1.PerformStep()
End Sub)
' ... other code ...
End Function
End Class
Then pass in "Me" when you call it:
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' ... other code ...
Await Cheque.MultiProcessCheques(Me)
' ... other code ...
End Sub
I have a program with a long running Active Directory query. I wanted to take advantage of VB.NET's Async technology, but when I converted my function to Async, I started getting an InvalidCastException. When I switch back, the error goes away. Why is Async causing an InvalidCastException for my COM object?
Exception Message:
Unable to cast COM object of type 'System.__ComObject' to interface type 'IDirectorySearch'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{109BA8EC-92F0-11D0-A790-00C04FD8D5A8}' failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).
This must be happening somewhere within the core library, because I don't have any references to IDirectorySearch in my code. Indeed, the stack trace is not very illuminating:
Here's where the exception is thrown (according to the debugger):
Private Overloads Sub OnPropertyChanged(propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Here's the actual code. I've created two versions to demonstrate the code before (FindAll1) and after (FindAll2) async:
Private Async Sub FindAllButton_Click(sender As Object, e As RoutedEventArgs) Handles FindAllButton.Click
'Me.Entries = Await FindAll1(Me.FilterText) ' Works
Me.Entries = Await FindAll2(Me.FilterText) ' Doesn't Work
End Sub
Private Async Function FindAll1(filterText As String) As Task(Of IEnumerable(Of DirectoryEntryWrapper))
Dim l_searcher As New DirectorySearcher()
l_searcher.SizeLimit = Me.QuerySizeLimit
l_searcher.Filter = filterText
Me.IsLoading = True
Dim l_results =
From result In l_searcher.FindAll().Cast(Of SearchResult)()
Select entry =
New DirectoryEntryWrapper(result.GetDirectoryEntry(), AddressOf DirectoryEntryWrapperEventHandler)
Order By entry.Name
Me.IsLoading = False
Return l_results
End Function
Private Async Function FindAll2(filterText As String) As Task(Of IEnumerable(Of DirectoryEntryWrapper))
Dim l_searcher As New DirectorySearcher()
l_searcher.SizeLimit = Me.QuerySizeLimit
l_searcher.Filter = filterText
Me.IsLoading = True
Dim l_results =
Await Task.Run(
Function() _
From result In l_searcher.FindAll().Cast(Of SearchResult)()
Select entry =
New DirectoryEntryWrapper(result.GetDirectoryEntry(), AddressOf DirectoryEntryWrapperEventHandler)
Order By entry.Name
)
Me.IsLoading = False
Return l_results
End Function