Basic asynchronous operations (VB.NET)
Introduction
Developers, novice and hobbyist can avoid performance bottlenecks and enhance the overall responsiveness of your application by using asynchronous programming. Asynchrony is essential for activities that are potentially blocking, such as when your application accesses the web or local.
Access to a web or local resource sometimes is slow or delayed. If such an activity blocks within a synchronous process, the entire application must wait. In an asynchronous process, the application can continue with other work that does not depend on the web resource until the potentially blocking task finishes.
There are many long time coders using BackgroundWorker class which works for desktop and web applications while working with the await operator and various .NET classes which offer asynchronous methods and/or running synchronous operations from a Task offers more flexibility. Intent is to provide simple to intermediate code samples to assist developers, novice and hobbyist a framework to work with asynchronous operations using the await operator in tangent with Task.
Following code samples is a good start while afterwards dissecting the code by reading Microsoft documentation will allow developers, novice, hobbyist, and understanding of how things work so instead of trial and error attempting to make a synchronous operation an asynchronous operation easier, less time figuring and more time coding.
In the majority of code samples, there is little in-depth explanations which is why it’s critical to read the links under recommended reading.
Various ways to perform asynchronous
The following code samples start with basics and lead into intermediate level example which in some cases will use delegates and custom EventArgs. Using delegates and EventArgs allow code executing in another class to allow the caller, in these cases a windows form to subscribe to events and react.
BackgroundWorker simple
Although a BackgroundWorker can be created for Windows forms project by dropping one on the form via the toolbox, here one is created by hand coding.
Private waitFor As Integer = 2000
Private Sub BackGroundWorkerButton_Click(sender As Object, e As EventArgs)
Dim bgw = New BackgroundWorker()
AddHandler bgw.DoWork,
Sub()
Thread.Sleep(waitFor)
GeneralResultsLabel.Text = "Success from background worker"
End Sub
AddHandler bgw.RunWorkerCompleted,
Sub()
MessageBox.Show("Hi from the UI thread for backgrounder worker!")
End Sub
bgw.RunWorkerAsync()
End Sub
Running the above code will keep the application responsive even though Thread.Sleep pauses all execution of the current thread when used outside of, in this case the DoWork event of the BackgroundWorker while in DoWork the code is asynchronous. Consider Thread.Sleep as a I/O-intense process. GeneralResultsLabel will report work has completed yet since GeneralResultsLabel is in a different thread then DoWork an exception is thrown
Cross-thread operation not valid: Control 'GeneralResultsLabel' accessed from a thread other than the thread it was created on.'
To get around this with MethodInvoker which works for Task too, Note that the sub can be a one liner, it's done in a body to keep the code on screen well formatted.
Private Sub BackGroundWorkerButton_Click(sender As Object, e As EventArgs)
Dim bgw = New BackgroundWorker()
AddHandler bgw.DoWork,
Sub()
Thread.Sleep(waitFor)
GeneralResultsLabel.Invoke(New MethodInvoker(
Sub()
GeneralResultsLabel.Text = "Success from background worker"
End Sub))
End Sub
AddHandler bgw.RunWorkerCompleted,
Sub()
MessageBox.Show("Hi from the UI thread for backgrounder worker!")
End Sub
bgw.RunWorkerAsync()
End Sub
Now to do the same with async and await, much less code and easy syntax.
Private Async Sub TaskRunInvokeButton_Click(sender As Object, e As EventArgs) _
Handles TaskRunInvokeButton.Click
Await Task.Run(
Sub()
Thread.Sleep(waitFor)
GeneralResultsLabel.InvokeIfRequired(Sub(label) label.Text = "Success from TaskRunInvokeButton")
End Sub)
MessageBox.Show("Hi from the UI thread in Button3!")
End Sub
The following example uses a async method of HttpClient, GetStringAsync to count a specific token from a web page.
Private ReadOnly _httpClient As New HttpClient()
''' <summary>
''' Adapted from Microsoft code sample
''' https://docs.microsoft.com/en-us/dotnet/csharp/async
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Async Sub WebExampleButton_Click(sender As Object, e As EventArgs)
WebResultLabel.Invoke(New MethodInvoker(Sub() WebResultLabel.Text = "Working..."))
Await Task.Delay(500)
Dim count = Await GetDotNetCount()
WebResultLabel.Invoke(New MethodInvoker(Sub() WebResultLabel.Text = $"Count of time .NET appears {count}"))
End Sub
Public Async Function GetDotNetCount() As Task(Of Integer)
' Suspends GetDotNetCount() to allow the caller (the web server)
' to accept another request, rather than blocking on this one.
Dim html = Await _httpClient.GetStringAsync("https://dotnetfoundation.org")
Return Regex.Matches(html, "\.NET").Count
End Function
Enumerate files with cancellation
A common operation with many coders is to search for something in files where the search is against an entire folder structure rather than a single folder. The method Directory.EnumerateFiles Method is perfect for this although the method is synchronous and can not easily be cancelled if the user decided to stop and perhaps try with a different search.
In the following example:
- The search will be performed in a class called by a form.
- Communication between the class and form will be done using a delegate.
- Canceling option is done using a CancellationTokenSource.
- Reentry is considered in the event a user decides to run again without cancelling a current running search.
- Proper exception handling is covered in the event of a runtime exception e.g. insufficient permissions to read a folder or file.
Communication is handled by the following class and delegate.
Namespace Classes
Public Class MonitorProgressArgs
Inherits EventArgs
Public Sub New(fileName As String)
CurrentFileName = fileName
End Sub
Public ReadOnly Property CurrentFileName() As String
End Class
End Namespace
Namespace Classes
Public Class DelegatesModule
Public Delegate Sub OnIterate(args As MonitorProgressArgs)
End Class
End NameSpace
The folder to search for test will be C:\Program Files (x86) searching text files for "THE SOFTWARE IS PROVIDED".
- To prevent reentry the button is disabled, note that in the event of a runtime exception the finally section of the try will enable the button
- The catch OperationCanceledException is thrown if Cancel method is called on the cancellation token in another button click event.
- In the method IterateFolders in the class Operations a check is done to detect if the operation should be cancelled e.g. token.IsCancellationRequested.
- The second catch is for all other exceptions such as insufficient permissions on a folder or file.
- When creating a new instance of the Operations class a subscription is done to listen for updates which are received by IterateFolders event.
- Note the check for _cancellationTokenSource.IsCancellationRequested, if true this means the operation has been cancelled which means an new instance of the CancellationTokenSource needs to be created, otherwise a runtime exception is thrown.
Private Async Sub StartButton_Click(sender As Object, e As EventArgs) Handles StartButton.Click
FolderNameListBox.DataSource = Nothing
ErrorListBox.Items.Clear()
If _cancellationTokenSource.IsCancellationRequested Then
_cancellationTokenSource.Dispose()
_cancellationTokenSource = New CancellationTokenSource()
End If
Dim operations As New Operations
AddHandler operations.OnIterate, AddressOf IterateFolders
Try
Dim foundFiles = Await operations.IterateFolders(
folderName,
searchFor,
_cancellationTokenSource.Token)
If Not operations.FolderExists Then
MessageBox.Show($"{folderName}{Environment.NewLine} does not exists")
ElseIf foundFiles.Count = 0 Then
MessageBox.Show("No matches")
End If
FolderNameListBox.DataSource = foundFiles
CurrentFileLabel.Text = ""
Catch oce As OperationCanceledException
'
' Land here from token.ThrowIfCancellationRequested()
' thrown in Run method from a cancel request in this
' form's Cancel button
'
MessageBox.Show("Operation cancelled")
Catch ex As Exception
'
' Handle any unhandled exceptions
'
MessageBox.Show(ex.Message)
Finally
'
' Success or failure reenable the Run button
'
StartButton.Enabled = True
End Try
End Sub
Copy file asynchronous with progress
The following example demonstrates how to copy a file asynchronously using streams while there is Stream.CopyToAsync Method but has no callback to report progress to the user interface were a progress indicator for larger files allows the user to know how far the copy is in the operation. In the following example there is no cancellation yet cancellation for this follows the exact same pattern done in the enumerate folders/files above.
The following language extension method takes a stream for the source file and a stream for the destination file followed by bytes to read for a buffer then an action which is the callback to the user interface to report the current percent done in a ProgressBar.
Notes
- The buffer size is variable, in this case the buffer size is hard coded for this simple example while for a real world solution a bit of math can be used to determine the proper buffer size for different size files.
- Await Task.Delay(200) should not be used for real world application, since the file used in this sample is only 5,000 plus lines without the delay the copy process would be done in a split second while for a file with several hundred or more lines the progress will be a little slower.
- If not dealing with a very large file use a synchronously code solution instead.
Imports System.IO
Imports System.Runtime.CompilerServices
Namespace Extensions
Public Module StreamExtensions
''' <summary>
''' Copy a file async via open streams
''' </summary>
''' <param name="source">Current open stream for file to copy</param>
''' <param name="destination">Current open stream for destination file</param>
''' <param name="bufferSize">Buffer size for reading</param>
''' <param name="progress">Action to report progress</param>
''' <returns></returns>
''' <remarks>
''' Buffer size should be adjusted according to your needs and could be
''' a calculated number per file
''' </remarks>
<Extension>
Public Async Function CopyToWithProgressAsync(
source As Stream,
destination As Stream,
bufferSize As Integer,
Optional ByVal progress As Action(Of Integer) = Nothing) As Task
Dim buffer = New Byte(bufferSize - 1) {}
Dim total = 0
Dim amountRead As Integer
Do
amountRead = 0
Do While amountRead < bufferSize
Dim numBytes = Await source.ReadAsync(buffer, amountRead, bufferSize - amountRead)
If numBytes = 0 Then
Exit Do
End If
amountRead += numBytes
'
' Only for demonstrating, remove for your code
'
Await Task.Delay(200)
Loop
total += amountRead
Await destination.WriteAsync(buffer, 0, amountRead)
If progress IsNot Nothing Then
progress(total)
End If
Loop While amountRead = bufferSize
End Function
End Module
End Namespace
Form code which uses a delegate same as in the enumerate folders/files.
Imports System.IO
Imports CopyFileAsync.Classes
Public Class Form1
Private Async Sub CopyFileButton_Click(sender As Object, e As EventArgs) _
Handles CopyFileButton.Click
Dim operations As New FileOperations
Dim sourceFileName As String =
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Names1.txt")
Dim destinationFileName As String =
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Names2.txt")
ProgressBar1.Maximum = CType(operations.GetFileBytes(sourceFileName), Integer)
AddHandler operations.OnIterate, AddressOf OnIterate
Dim success = Await operations.CopyFile(sourceFileName, destinationFileName)
If success Then
MessageBox.Show("Done")
Else
MessageBox.Show(operations.LastExceptionMessage)
End If
End Sub
''' <summary>
''' Call back to set percent done in ProgressBae
''' </summary>
''' <param name="args"></param>
Private Sub OnIterate(args As MonitorProgressArgs)
ProgressBar1.Value = args.Current
End Sub
End Class
Asynchronously database connection
Using conventional data providers to open a connection e.g. SQL-Server using SqlConnection for instance if the database server can not be found in most cases there will be a frozen form while the connection attempts to connection. In these cases using OpenAsync for SQL-Server connection in an asynchronous method the application will remain responsive. When working with a DataTable to load data SqlCommand.ExecuteReaderAsync Method will keep things responsive. To dig deeper (not covered here), when using a SqlDataReader review GetFieldValueAsync which is passed in the ordinal position of the column to read and a CancellationToken where the first example for enumerating folders and files show how to setup and pass a CancellationToken to a class method.
To get a feel for how the above works, in the following class there are two connection strings, one will work while the other will not. Try both to get a feel for what happens in both cases.
Imports System.Data.SqlClient
Namespace Classes
Public Class DataOperations
Private Shared ConnectionStringWorks As String =
"Data Source=.\SQLEXPRESS;" &
"Initial Catalog=NorthWindAzureForInserts;" &
"Integrated Security=True"
Private Shared ConnectionStringFails As String =
"Data Source=.\SQLEXPRESS_BAD;" &
"Initial Catalog=NorthWindAzureForInserts;" &
"Integrated Security=True"
Public Shared Async Function ReadCustomers() As Task(Of DataTable)
Return Await Task.Run(Async Function()
Dim customersTable = New DataTable()
Using cn = New SqlConnection(ConnectionStringFails)
Using cmd = New SqlCommand() With {.Connection = cn}
cmd.CommandText = SelectStatement()
Await cn.OpenAsync()
customersTable.Load(Await cmd.ExecuteReaderAsync())
End Using
End Using
customersTable.Columns.Cast(Of DataColumn).
Where(Function(column) column.ColumnName.Contains("Id")).
ToList().
ForEach(Sub(column)
column.ColumnMapping = MappingType.Hidden
End Sub)
Return customersTable
End Function)
End Function
Private Shared Function SelectStatement() As String
Return <SQL>
SELECT Cust.CustomerIdentifier,
Cust.CompanyName,
Cust.ContactId,
Contacts.FirstName,
Contacts.LastName,
Cust.ContactTypeIdentifier,
CT.ContactTitle,
Cust.Address AS Street,
Cust.City,
Cust.PostalCode,
Cust.CountryIdentifier,
Countries.Name AS CountryName,
Cust.ModifiedDate
FROM Customers AS Cust
INNER JOIN ContactType AS CT ON Cust.ContactTypeIdentifier = CT.ContactTypeIdentifier
INNER JOIN Contacts ON Cust.ContactId = Contacts.ContactId
INNER JOIN Countries ON Cust.CountryIdentifier = Countries.CountryIdentifier
</SQL>.Value
End Function
End Class
End Namespace
Form code is very simple, create an instance of the above class and invoke the method ReadCustomers following by setting the resulting DataTable to a BindingSource which then becomes the data source of a DataGridView,
Imports SqlServerSimpleAsync.Classes
Public Class Form1
Private customersBindingSource As New BindingSource
Private Async Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown
Try
Dim customersTable = Await DataOperations.ReadCustomers()
customersBindingSource.DataSource = customersTable
DataGridView1.DataSource = customersBindingSource
Catch ex As Exception
'
' Handle any unhandled exceptions
'
MessageBox.Show(ex.Message)
End Try
End Sub
End Class
Read file asynchronously
Working with files is a common practice and reading files asynchronously is a common idea although in many cases a better idea is to split/chunk a larger file into smaller files. If reading a larger file and chunking up a file is undesirable the following example will demonstrate reading a file.
The key here is using StreamReader.ReadLineAsync along with a CancellationToken no different in prior examples.
Imports System.IO
Imports System.Threading
Namespace Classes
Public Class FileOperations
Private ReadOnly _fileName As String =
Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "Data.txt")
Public Event OnMonitor As DelegatesModule.MonitorHandler
''' <summary>
''' Read file async without any assertions on if there are enough
''' columns, data present e.g null values etc.
''' </summary>
''' <param name="token"></param>
''' <returns></returns>
Public Async Function ReadFile(token As CancellationToken) As Task
Dim lineIndex = 1
'cn.Open
Dim currentLine As String
Using reader As StreamReader = File.OpenText(_fileName)
While Not reader.EndOfStream
currentLine = Await reader.ReadLineAsync()
Dim parts = currentLine.Split(","c)
Dim person = New Person With {
.FirstName = parts(0),
.MiddleName = parts(1),
.LastName = parts(2),
.Street = parts(3),
.City = parts(4),
.State = parts(5),
.PostalCode = parts(6),
.EmailAddress = parts(7)
}
OnMonitorEvent?.Invoke(New MonitorArgs(person.FieldArray(), lineIndex))
lineIndex += 1
Await Task.Delay(1, token)
If token.IsCancellationRequested Then
token.ThrowIfCancellationRequested()
End If
End While
End Using
End Function
End Class
End Namespace
Form code which follows along with prior examples in regards to a) monitoring progress b) handling cancellation request.
Imports System.IO
Imports System.Net.NetworkInformation
Imports System.Threading
Imports ReadingDelimitedFiles.Classes
Public Class Form1
Private _fileName As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data.txt")
Private _cancellationTokenSource As New CancellationTokenSource()
Private Async Sub ReadFileButton_Click(sender As Object, e As EventArgs) Handles ReadFileButton.Click
ReadFileButton.Enabled = False
StatusLabel.Text = ""
If DataGridView1.Rows.Count > 0 Then
DataGridView1.Rows.Clear()
End If
If _cancellationTokenSource.IsCancellationRequested Then
_cancellationTokenSource.Dispose()
_cancellationTokenSource = New CancellationTokenSource()
End If
Try
Dim operation = New FileOperations
AddHandler operation.OnMonitor, AddressOf MonitorProgress
Await operation.ReadFile(_cancellationTokenSource.Token)
Catch oce As OperationCanceledException
'
' Land here from token.ThrowIfCancellationRequested()
' thrown in Run method from a cancel request in this
' form's Cancel button
'
MessageBox.Show("Operation cancelled")
Catch ex As Exception
'
' Handle any unhandled exceptions
'
MessageBox.Show(ex.Message)
Finally
'
' Success or failure reenable the Run button
'
ReadFileButton.Enabled = True
End Try
End Sub
Private Sub MonitorProgress(args As MonitorArgs)
DataGridView1.Rows.Add(args.PersonArray)
StatusLabel.Text = $"Reading line {args.CurrentIndex}"
End Sub
Private Sub CancelButton_Click(sender As Object, e As EventArgs) Handles CancelButton.Click
If Not ReadFileButton.Enabled Then
_cancellationTokenSource.Cancel()
ReadFileButton.Enabled = True
End If
End Sub
Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown
StatusLabel.Text = ""
End Sub
End Class
C# helpers
There is a code sample for running multiple task and waiting for all to finish. A C# method below is used. There are two reasons, the first, surely most VB.NET developer will search for assistance on the web and many times there is a solution for C# that for one or more reasons does not fit into VB.NET. In these cases consider creating a C# class project, drop the code in and reference in a VB.NET project.
public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2) => (await task1, await task2);
VB.NET helpers
The following code module contains method to perform InvokeRequired which some may find useful.
Summary
Most common operations presented for asynchronous operations, which by following the examples and reading Microsoft documentation will provide a decent foundation for working these concepts into robust applications. There are other things to learn as the presented samples are basics, for instance running multiple task and waiting for all of them to finish. There is a basic examples found in the companion source code here.
See also
Azure Functions: Asynchronous Programming
.NET: What does async & await generate?
External recommended reading
Asynchronous programming
Task asynchronous programming model
Asynchronous programming with Async and Await (Visual Basic)
Source code
Clone or download source from the following GitHub repository.