Share via


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".

  1. 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
  2. The catch OperationCanceledException is thrown if Cancel method is called on the cancellation token in another button click event.
    1. In the method IterateFolders in the class Operations a check is done to detect if the operation should be cancelled e.g. token.IsCancellationRequested.
  3. The second catch is for all other exceptions such as insufficient permissions on a folder or file.
  4. When creating a new instance of the Operations class a subscription is done to listen for updates which are received by IterateFolders event
  5. 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?
 

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.