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
Related
The application creates a collection of tasks to obtain folder info from a specific folder path.
This code performs well, but my issue is that the UI is freezing while adding tasks to the list, especially in the while loop, until all tasks are complete.
Here's the code:
Async Function ProcessFolders(ct As CancellationToken, FolderList As IEnumerable(Of
String)) As Task
' Create a collection of tasks
Dim processFoldersQuery As IEnumerable(Of Task(Of String())) =
From folder In FolderList Select GetFolderInfo(folder, ct)
Dim processQuery As List(Of Task(Of String())) = processFoldersQuery.ToList()
' Run through tasks
While processQuery.Count > 0
Dim finishedTask As Task(Of String()) = Await Task.WhenAny(processQuery)
processQuery.Remove(finishedTask)
Dim result = Await finishedTask
folder_dt.Rows.Add(Nothing, result(0), result(1), result(2))
End While
End Function
Async Function GetFolderInfo(folder As String, ct As CancellationToken) As Task(Of
String())
Return Await Task.Run(Async Function()
Dim folder_info = New DirectoryInfo(folder)
Dim result() As String = {folder_info.Name, Await
GetFileMd5(folder, False), Await GetDirectorySize(folder)}
Return result
End Function)
End Function
How to achieve this without UI freezing? I have been looking into parallel and async loops and various different async techniques, but I am not sure how to implement them in this situation.
GetFileMd5() and GetDirectorySize() functions below:
Shared Async Function GetDirectorySize(path As String) As Task(Of Long)
Return Await Task.Run(Function()
Return Directory.GetFiles(path, "*", SearchOption.AllDirectories).Sum(Function(t) (New FileInfo(t).Length))
End Function)
End Function
Private Async Function GetFileMd5(file_name As String, convert_file As Boolean) As Task(Of String)
Return Await Task.Run(Function()
Dim byteHash() As Byte
Dim ByteArrayToString = Function(arrInput() As Byte)
Dim sb As New Text.StringBuilder(arrInput.Length * 2)
For i As Integer = 0 To arrInput.Length - 1
sb.Append(arrInput(i).ToString("X2"))
Next
Return sb.ToString().ToLower
End Function
If convert_file Then
byteHash = Md5CSP.ComputeHash(File.OpenRead(file_name))
Return ByteArrayToString(byteHash)
End If
If convert_file = False Then byteHash = Md5CSP.ComputeHash(System.Text.Encoding.Unicode.GetBytes(file_name)) : Return ByteArrayToString(byteHash)
Return Nothing
End Function)
End Function
A couple of examples that may mitigate a form of lagging you're experiencing when the number of items in the list of folders you're processing increases.
In the current code, these two lines:
processQuery.Remove(finishedTask)
' [...]
folder_dt.Rows.Add(Nothing, result(0), result(1), result(2))
are executed in the UI Thread. In my view, the former is troublesome.
It's also not clear how the DataTable is used. It appears that when ProcessFolders() (which doesn't return that object) returns, a previously existing DataTable is blindly assigned to something.
ProcessFolders() shouldn't know about this, already existing, DataTable.
In the first example, the entry method (renamed ProcessFoldersAsync()), creates a DataTable that is meant to contain the data it generates and returns this DataTable.
You should pass the CancellationToken also to the GetFileMd5() and GetDirectorySize() methods.
Private cts As CancellationTokenSource = Nothing
Private folder_dt As DataTable = Nothing
Private Async Sub SomeButton_Click(sender As Object, e As EventArgs) Handles SomeButton.Click
Dim listOfFolders As New List(Of String) = [Some source of URIs]
cts = New CancellationTokenSource
Using cts
' Await resumes in the UI Thread
folder_dt = Await ProcessFoldersAsync(listOfFolder, cts.Token)
[Some Control].DataSource = folder_dt
End Using
End Sub
Async Function ProcessFoldersAsync(folderList As IEnumerable(Of String), dtSchema As DataTable, token As CancellationToken) As Task(Of DataTable)
Dim processQuery As New List(Of Task(Of Object()))
For Each f In folderList
processQuery.Add(GetFolderInfoAsync(f, token))
Next
If token.IsCancellationRequested Then Return Nothing
Try
' Resumes on a ThreadPool Thread
Await Task.WhenAll(processQuery).ConfigureAwait(False)
' Generate a new DataTable and fills it with the results of all Tasks
' This code executes in a ThreadPool Thread
Dim dt = CreateDataTable()
For Each obj In processQuery
If obj.Result IsNot Nothing Then dt.Rows.Add(obj.Result)
Next
Return dt
Catch ex As TaskCanceledException
Return Nothing
End Try
End Function
Async Function GetFolderInfoAsync(folder As String, token As CancellationToken) As Task(Of Object())
token.ThrowIfCancellationRequested()
Dim folderName = Path.GetFileName(folder)
Return {Nothing, folderName, Await GetFileMd5(folder, False), Await GetDirectorySize(folder)}
End Function
In the second example, an IProgress<T> delegate is used perform the updates it receives from the GetFolderInfoAsync() method.
In this case, the DataTable already exists and could be already assigned to / used by Controls. In this case, the updates are performed in real time, as the async methods return their results, which may happen at different times.
This method may slow down the process, overall.
ProcessFoldersAsync() passes the Progress object delegate to GetFolderInfoAsync(), which calls Report() method with the data object it has elaborated.
The UpdatedDataTable() delegate method adds the new data arrived to the existing DataTable
Note that, in this case, if the Tasks are canceled, the updates already stored are retained (unless you decide otherwise, that is. You can always set the DataTable to null when you catch a TaskCanceledException exception)
' [...]
Private objLock As New Object()
Private updater As IProgress(Of IEnumerable(Of Object))
Private Async Sub SomeButton_Click(sender As Object, e As EventArgs) Handles SomeButton.Click
Dim listOfFolders As New List(Of String) = [Some source of URIs]
folder_dt = CreateDataTable()
[Some Control].DataSource = folder_dt
cts = New CancellationTokenSource
Using cts
updater = New Progress(Of IEnumerable(Of Object))(Sub(data) UpdatedDataTable(data))
Try
Await ProcessFoldersAsync(listOfFolder, updater, cts.Token)
Catch ex As TaskCanceledException
Debug.WriteLine("Tasks were canceled")
End Try
End Using
End Sub
Async Function ProcessFoldersAsync(folderList As IEnumerable(Of String), updater As IProgress(Of IEnumerable(Of Object)), token As CancellationToken) As Task
Dim processQuery As New List(Of Task)
For Each f In folderList
processQuery.Add(GetFolderInfoAsync(f, updater, token))
Next
token.ThrowIfCancellationRequested()
Await Task.WhenAll(processQuery).ConfigureAwait(False)
End Function
Async Function GetFolderInfoAsync(folder As String, progress As IProgress(Of IEnumerable(Of Object)), token As CancellationToken) As Task
token.ThrowIfCancellationRequested()
Dim folderName = Path.GetFileName(folder)
Dim result As Object() = {Nothing, folderName, Await GetFileMd5(folder, False), Await GetDirectorySize(folder)}
progress.Report(result)
End Function
Private Sub UpdatedDataTable(data As IEnumerable(Of Object))
SyncLock objLock
folder_dt.Rows.Add(data.ToArray())
End SyncLock
End Sub
Private Function CreateDataTable() As DataTable
Dim dt As New DataTable()
Dim col As New DataColumn("IDX", GetType(Long)) With {
.AutoIncrement = True,
.AutoIncrementSeed = 1,
.AutoIncrementStep = 1
}
dt.Columns.Add(col)
dt.Columns.Add("FolderName", GetType(String))
dt.Columns.Add("MD5", GetType(String))
dt.Columns.Add("Size", GetType(Long))
Return dt
End Function
I am working on coronavirus statistics dashboard as university project, and I have some problems with asynchronous source data download from sites with statistics.
Well, I failed to understand how to do it myself.
I tried to create my own class with function what will create multiple async web requests
and then wait until they all finished, then return results of all these requests.
Imports System.Net.WebClient
Imports System.Net
Public Class AsyncDownload
Private result As New Collection
Private Sub DownloadCompletedHander(ByVal sender As Object, ByVal e As System.Net.DownloadStringCompletedEventArgs)
If e.Cancelled = False AndAlso e.Error Is Nothing Then
Dim myString As String = CStr(e.Result)
result.Add(myString, sender.Headers.Item("source"))
End If
End Sub
Public Function Load(sources As Array, keys As Array) As Collection
Dim i = 0
Dim WebClients As New Collection
While (i < sources.Length)
Dim newClient As New WebClient
newClient.Headers.Add("source", keys(i))
newClient.Headers.Add("sourceURL", sources(i))
AddHandler newClient.DownloadStringCompleted, AddressOf DownloadCompletedHander
WebClients.Add(newClient)
i = i + 1
End While
i = 1
For Each client As WebClient In WebClients
Dim url As String = client.Headers.Item("sourceURL")
client.DownloadStringAsync(New Uri(url))
Next
While (result.Count < WebClients.Count)
End While
Return result
End Function
End Class
And it is used in:
Dim result As New Collection
Private Sub test() Handles Me.Load
Dim downloader As New CoronaStatisticsGetter.AsyncDownload
result = downloader.Load({"https://opendata.digilugu.ee/covid19/vaccination/v3/opendata_covid19_vaccination_total.json"}, {"Nationalwide Data"})
End Sub
It should work like:
I create a new instance of my class.
Calling function Load of this class
Funciton Load creates instances of System.Net.WebClient for each url and adds as handler DownloadCompletedHander
Function Load goes calls DownloadStringAsync of each client
Function Load waits in While loop until result collection items count is not as big as number of url on input
If item count in result is same as urls number that means what everything is downloaded, so it breaks loop and returns all requested data
The problem is that it doesn't work, it just endlessly remain in while loop, and as I see using debug collection result is not updated (its size is always 0)
Same time, when I try to asynchronously download it without using my class, everything works fine:
Private Sub Download() 'Handles Me.Load
Dim wc As New System.Net.WebClient
wc.Headers.Add("source", "VaccinationByAgeGroup")
AddHandler wc.DownloadStringCompleted, AddressOf DownloadCompletedHander
wc.DownloadStringAsync(New Uri("https://opendata.digilugu.ee/covid19/vaccination/v3/opendata_covid19_vaccination_agegroup.json"))
End Sub
Could somebody tell me please why it is not working and where is the problem?
The following shows how one can use System.Net.WebClient with Task to download a string (ie: data) from a URL.
Add a project reference (System.Net)
VS 2019:
In VS menu, click Project
Select Add reference...
Select Assemblies
Check System.Net
Click OK
Create a class (name: DownloadedData.vb)
Public Class DownloadedData
Public Property Data As String
Public Property Url As String
End Class
Create a class (name: HelperWebClient.vb)
Public Class HelperWebClient
Public Async Function DownloadDataAsync(urls As List(Of String)) As Task(Of List(Of DownloadedData))
Dim allTasks As List(Of Task) = New List(Of Task)
Dim downloadedDataList As List(Of DownloadedData) = New List(Of DownloadedData)
For i As Integer = 0 To urls.Count - 1
'set value
Dim url As String = urls(i)
Debug.WriteLine(String.Format("[{0}]: Adding {1}", i, url))
Dim t = Task.Run(Async Function()
'create new instance
Dim wc As WebClient = New WebClient()
'await download
Dim result = Await wc.DownloadStringTaskAsync(url)
Debug.WriteLine(url & " download complete")
'ToDo: add desired code
'add
downloadedDataList.Add(New DownloadedData() With {.Url = url, .Data = result})
End Function)
'add
allTasks.Add(t)
Next
For i As Integer = 0 To allTasks.Count - 1
'wait for a task to complete
Dim t = Await Task.WhenAny(allTasks)
'remove from List
allTasks.Remove(t)
'write data to file
'Note: The following is only for testing.
'The index in urls won't necessarily correspond to the filename below
Dim filename As String = System.IO.Path.Combine("C:\Temp", String.Format("CoronavirusData_{0:00}.txt", i))
System.IO.File.WriteAllText(filename, downloadedDataList(i).Data)
Debug.WriteLine($"[{i}]: Filename: {filename}")
Next
Debug.WriteLine("all tasks complete")
Return downloadedDataList
End Function
End Class
Usage:
Private Async Sub btnRun_Click(sender As Object, e As EventArgs) Handles btnRun.Click
Dim helper As HelperWebClient = New HelperWebClient()
Dim urls As List(Of String) = New List(Of String)
urls.Add("https://opendata.digilugu.ee/covid19/vaccination/v3/opendata_covid19_vaccination_total.json")
urls.Add("https://api.covidtracking.com/v2/states.json")
urls.Add("https://covidtrackerapi.bsg.ox.ac.uk/api/v2/stringency/date-range/2020-01-01/2022-03-01")
urls.Add("http://covidsurvey.mit.edu:5000/query?age=20-30&gender=all&country=US&signal=locations_would_attend")
Dim downloadedDataList = Await helper.DownloadDataAsync(urls)
Debug.WriteLine("Complete")
End Sub
Resources:
How do I wait for something to finish in C#?
How should Task.Run call an async method in VB.NET?
VB.net ContinueWith
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'm trying to call a web service and have my code wait for that service to return a result (or timeout). My project is Silverlight 5 with Web Services using .NET 4.0 and I'm running this project under VS 2012 with the Microsoft.Bcl.Async.1.0.16\lib\sl4\Microsoft.Threading.Tasks.dll ... Task.Extensions.dll ... and Extensions.Silverlight.dll.
This is how I've been doing it and it's working, but I'm trying to figure out how to change my code so that I can use the Async/Await process. The web service reference is configured to return ObservableCollection and Generic.Dictionary with Reuse types in all referenced assemblies.
Some of my code I need to convert to Async/Await:
Private _Units As Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units)
Public Property Units() As Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units)
Get
Return _Units
End Get
Set(ByVal value As Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units))
_Units = value
OnPropertyChanged(New PropertyChangedEventArgs("Units"))
End Set
End Property
Public Sub ReadUnits()
Try
' Client is required
If Not Me.Client Is Nothing Then
' User is required
If Not Me.User Is Nothing Then
' Must be a real Client
If Me.Client.ClientID > 0 Then
' My have a sites
If Not Me.Site Is Nothing Then
' Call the web service relative to where this application is running
Dim webServiceURI As New Uri("../WebServices/Unit.svc", UriKind.RelativeOrAbsolute)
Dim webServiceAddress As New EndpointAddress(webServiceURI)
' Setup web Service proxy
Dim wsUnits As New DC.SL.Services.WebServiceUnit.UnitClient
wsUnits.Endpoint.Address = webServiceAddress
' Add event handler so we can trap for web service completion
AddHandler wsUnits.LoadsCompleted, AddressOf LoadUnitsCompleted
' Call web service to get Sites the user has access to
wsUnits.LoadsAsync(Me.Client, Me.Site.SiteID, Me.Size.SizeID, Me.RentalType.RentalTypeID, Me.UnitState)
End If
End If
End If
End If
Catch ex As Exception
Dim Problem As New DC.SL.Tools.Errors(ex)
End Try
End Sub
Private Sub LoadUnitsCompleted(ByVal sender As Object, ByVal e As DC.SL.Services.WebServiceUnit.LoadsCompletedEventArgs)
Try
If Not IsNothing(e.Result) Then
Me.Units = e.Result
If Me.Units.Count > 0 Then
Me.Unit = Me.Units.Item(0)
End If
End If
Catch ex As Exception
Dim Problem As New DC.SL.Tools.Errors(ex)
End Try
End Sub
Still not getting this to work ... here is what I have now, but the problem remains ... UI thread execution continues and does NOT wait for the Web Service call to finish.
Calling code:
ReadUnitsAsync().Wait(3000)
Here is the updated code:
Public Async Function ReadUnitsAsync() As Task(Of Boolean)
Dim Results As Object = Await LoadReadUnitsAsync()
Return True
End Function
Public Function LoadReadUnitsAsync() As Task(Of System.Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units))
LoadReadUnitsAsync = Nothing
Dim tcs = New TaskCompletionSource(Of System.Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units))
' Client is required
If Not Me.Client Is Nothing Then
' User is required
If Not Me.User Is Nothing Then
' Must be a real Client associated
If Me.Client.ClientID > 0 Then
' Only get associated sites IF we don't have any defined
If Not Me.Site Is Nothing Then
' Call the web service relative to where this application is running
Dim webServiceURI As New Uri("../WebServices/Unit.svc", UriKind.RelativeOrAbsolute)
Dim webServiceAddress As New EndpointAddress(webServiceURI)
' Setup Site web Service proxy
Dim wsUnits As New DC.SL.Services.WebServiceUnit.UnitClient
wsUnits.Endpoint.Address = webServiceAddress
' Add event handler so we can trap for web service completion
AddHandler wsUnits.LoadUnitsCompleted, Sub(s, e)
If e.Error IsNot Nothing Then
tcs.TrySetException(e.Error)
ElseIf e.Cancelled Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(e.Result)
End If
End Sub
'' Set Busy Status
'BusyStack.Manage(ProcessManager.StackAction.Add, "ReadUnits", Me.IsWorking, Me.IsWorkingMessage)
' Call web service to get Sites the user has access to
wsUnits.LoadUnitsAsync(Me.Client, Me.Site.SiteID, Me.Size.SizeID, Me.RentalType.RentalTypeID, Me.UnitState)
Return tcs.Task
End If
End If
End If
End If
End Function
So here is the final code (abbreviated) that seems to be working to my goals (aka waiting for a Web Service to finish before proceeding).
Public Class UIUnits
Implements INotifyPropertyChanged, IDataErrorInfo
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Async Sub OnPropertyChanged(ByVal e As PropertyChangedEventArgs)
Dim propertyEventHandler As PropertyChangedEventHandler = PropertyChangedEvent
Try
If propertyEventHandler IsNot Nothing Then
RaiseEvent PropertyChanged(Me, e)
Select Case e.PropertyName
Case "Size"
Await ReadUnitsAsync()
DoSomethingElseAfterWebServiceCallCompletes()
Case Else
End Select
End If
Catch ex As Exception
Dim problem As New DC.SL.Tools.Errors(ex)
End Try
End Sub
...
Private _Units As Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units)
Public Property Units() As Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units)
Get
Return _Units
End Get
Set(ByVal value As Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units))
_Units = value
OnPropertyChanged(New PropertyChangedEventArgs("Units"))
End Set
End Property
...
Public Async Function ReadUnitsAsync() As Task(Of Boolean)
Me.Units = Await LoadReadUnitsAsync()
Return True
End Function
...
Public Function LoadReadUnitsAsync() As Task(Of System.Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units))
LoadReadUnitsAsync = Nothing
Dim tcs = New TaskCompletionSource(Of System.Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units))
' Client is required
If Not Me.Client Is Nothing Then
' User is required
If Not Me.User Is Nothing Then
' Must be a real Client associated
If Me.Client.ClientID > 0 Then
' Only get associated sites IF we don't have any defined
If Not Me.Site Is Nothing Then
' Call the web service relative to where this application is running
Dim webServiceURI As New Uri("../WebServices/Unit.svc", UriKind.RelativeOrAbsolute)
Dim webServiceAddress As New EndpointAddress(webServiceURI)
' Setup web Service proxy
Dim wsUnits As New DC.SL.Services.WebServiceUnit.UnitClient
wsUnits.Endpoint.Address = webServiceAddress
' Add event handler so we can trap for web service completion
AddHandler wsUnits.LoadUnitsCompleted, Sub(s, e)
If e.Error IsNot Nothing Then
tcs.TrySetException(e.Error)
ElseIf e.Cancelled Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(e.Result)
End If
End Sub
' Call web service
wsUnits.LoadUnitsAsync(Me.Client, Me.Site.SiteID, Me.Size.SizeID, Me.RentalType.RentalTypeID, Me.UnitState)
Return tcs.Task
End If
End If
End If
End If
End Function
In this case it's probably easiest to convert from the lowest level and work your way up. First, you need to define your own TAP-friendly extension methods on your service. VS will generate these for you if you are doing desktop development, but unfortunately it will not do this for Silverlight.
The MSDN docs describe how to wrap EAP (EAP is the pattern that uses *Async methods with matching *Completed events). If you have APM methods, it's even easier to wrap those into TAP (APM is the pattern that uses Begin*/End* method pairs).
Once you have a wrapper, e.g., LoadUnitsTaskAsync, change your method to call that instead of LoadUnitsAsync and Await the result. This will require your ReadUnits method to be Async, so change it to a Task-returning Function (and change its name from ReadUnits to ReadUnitsAsync). Next change all callers of ReadUnitsAsync so they Await its result. Repeat until you reach an actual event handler, which may be Async Sub (do not use Async Sub for any intermediate methods; use Async Function ... As Task instead).
I used this library to achieve async await in my Silverlight 5 project
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.