Page 1 of 1

Async/Await with Progress(Bar) and Cancellation Rate Topic: -----

#1 andrewsw  Icon User is online

  • the case is sol-ved
  • member icon

Reputation: 6374
  • View blog
  • Posts: 25,754
  • Joined: 12-December 12

Posted 14 February 2016 - 06:40 AM

This presents more of an example that you can study rather than a full blown tutorial because I'm at an early stage with this myself. It is an example of the use of the new Async and Await features (introduced in .NET 4.5):

MSDN said:

Visual Studio 2012 introduces a simplified approach, async programming, that leverages asynchronous support in the .NET Framework 4.5 and the Windows Runtime. The compiler does the difficult work that the developer used to do, and your application retains a logical structure that resembles synchronous code. As a result, you get all the advantages of asynchronous programming with a fraction of the effort.

Asynchronous Programming with Async and Await (C# and Visual Basic) :MSDN

My example (a WinForm application) is mainly a conversion of the C# example at this link, which I recommend studying:

Async in 4.5: Enabling Progress and Cancellation in Async APIs

The Telerik converter is very useful (although not flawless) if you want to convert C# to VB.NET.

My version is different (aside from being in VB) because I wanted to fully demonstrate the updating of a ProgressBar (the linked example doesn't fill-in these details), and I also iterate the files of subfolders rather than loading images. I find basic examples using Thread.Sleep unrealistic (and, therefore, unhelpful), and examples that require uploaded images are tricky for people to set-up and run. Anyone could run my example(s).

Note that my application iterates subfolders, so make sure that you first select a folder that contains a number of subfolders.

One of the reasons for my tutorial/example is that all of the following useful links are with C# code:

Async, Await and the UI problem
Task Parallel Library and async-await Functionality
Async and Await

Spoiler

Create a new WinForm application, mine was named AsyncWithProgress.

Attached Image

The controls are named frmAsyncProgress, txtPath, btnStart, btnCancel, barFileProgress and lblPercent. (I suppose 'pgb' would be a more consistent prefix for a ProgressBar, but 'bar' is too obvious to turn down.)

Here is the full first version of the code for you to study, but I include some notes below. This version doesn't use the Cancel button.
Imports System.IO

Public Class frmAsyncProgress

    Private Sub frmAsyncProgress_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        barFileProgress.Minimum = 0
        barFileProgress.Maximum = 100
        btnCancel.Enabled = False
    End Sub

    Private Async Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
        If String.IsNullOrWhiteSpace(txtPath.Text) Then
            MessageBox.Show("Provide a location first.", "Location")
            Exit Sub
        End If
        Dim sLocation As String = txtPath.Text.Trim()
        If Not Directory.Exists(sLocation) Then
            MessageBox.Show("Directory doesn't exist.", "Location")
            Exit Sub
        End If

        Dim progressIndicator = New Progress(Of Integer)(AddressOf UpdateProgress)
        btnStart.Enabled = False
        btnCancel.Enabled = True
        lblPercent.Text = "0%"

        Dim allFiles As Integer = Await AllSubfolderFiles(sLocation, progressIndicator)
        Debug.WriteLine(allFiles.ToString())        'the number of subfolders iterated
        btnStart.Enabled = True
        btnCancel.Enabled = False
    End Sub

    Private Async Function AllSubfolderFiles(location As String, progress As IProgress(Of Integer)) As Task(Of Integer)
        Dim dirsTotal As Integer = Directory.GetDirectories(location).Length
        Dim dirsFraction As Integer = Await Task(Of Integer).Run(Function()
                                                                     Dim counter As Integer = 0
                                                                     For Each subDir As String In Directory.GetDirectories(location)
                                                                         SubfolderFiles(subDir)
                                                                         counter += 1
                                                                         If progress IsNot Nothing Then
                                                                             progress.Report(counter * 100 / dirsTotal)
                                                                         End If
                                                                     Next

                                                                     Return counter
                                                                 End Function)
        Return dirsFraction
    End Function

    Private Sub UpdateProgress(value As Integer)
        barFileProgress.Value = value
        lblPercent.Text = (value / 100).ToString("#0.##%")
    End Sub

    Private Sub SubfolderFiles(location As String)
        'source: http://stackoverflow.com/questions/16237291/visual-basic-2010-continue-on-error-unauthorizedaccessexception#answer-16237749

        Dim paths = New Queue(Of String)()
        Dim fileNames = New List(Of String)()

        paths.Enqueue(location)

        While paths.Count > 0
            Dim sDir = paths.Dequeue()

            Try
                Dim files = Directory.GetFiles(sDir)
                For Each file As String In Directory.GetFiles(sDir)
                    fileNames.Add(file)
                Next

                For Each subDir As String In Directory.GetDirectories(sDir)
                    paths.Enqueue(subDir)
                Next
            Catch ex As UnauthorizedAccessException
                ' log the exception or ignore it
                Debug.WriteLine("Directory {0}  could not be accessed!", sDir)
            Catch ex As Exception
                ' log the exception or ...
                Throw
            End Try
        End While
        'could return fileNames collection
    End Sub
End Class


Let's look at the significant code in btnStart_Click. But first, notice the addition of the keyword Async in the method signature Private Async Sub btnStart_Click. This indicates that the method can run asynchronously, with the inclusion of Await somewhere within it. If Await does not occur within the method then you will receive a warning and the method will run synchronously.

To my primitive interpretation (I'm still learning) this makes more sense than the earlier Task (and Thread) based approaches. When we want to run a method asynchronously it is not usually the entire method that should run async. There is usually some essential setting-up required, before a particular long-running part of the method is initiated.
        Dim progressIndicator = New Progress(Of Integer)(AddressOf UpdateProgress)
        btnStart.Enabled = False
        btnCancel.Enabled = True
        lblPercent.Text = "0%"

        Dim allFiles As Integer = Await AllSubfolderFiles(sLocation, progressIndicator)
        Debug.WriteLine(allFiles.ToString())        'the number of subfolders iterated
        btnStart.Enabled = True
        btnCancel.Enabled = False


UpdateProgress is a simple method further down that performs the UI updating (of the ProgressBar), given an integer value:
    Private Sub UpdateProgress(value As Integer)
        barFileProgress.Value = value
        lblPercent.Text = (value / 100).ToString("#0.##%")
    End Sub


Although Progress(Of T) is maybe a class you haven't used before, we can see that it accepts a value of a certain kind, which can then be used to update the UI.
        Dim allFiles As Integer = Await AllSubfolderFiles(sLocation, progressIndicator)
        Debug.WriteLine(allFiles.ToString())        'the number of subfolders iterated
        btnStart.Enabled = True
        btnCancel.Enabled = False


Significantly, when you run the application (and provide a location in the TextBox and press Start), you'll notice that the Start button is not re-enabled until all the location's subfolders have been iterated, yet the UI remains responsive; try moving and resizing the form while the iterating is in progress. This is what the keywords Async and Await are achieving for us.

Notice that the Progress object (essentially, a delegate) is passed to the method AllSubfolderFiles. It is through this delegate that we are able to periodically update the UI.

Let's look at our Async method:
    Private Async Function AllSubfolderFiles(location As String, progress As IProgress(Of Integer)) As Task(Of Integer)
        Dim dirsTotal As Integer = Directory.GetDirectories(location).Length
        Dim dirsFraction As Integer = Await Task(Of Integer).Run(Function()
                                                                     Dim counter As Integer = 0
                                                                     For Each subDir As String In Directory.GetDirectories(location)
                                                                         SubfolderFiles(subDir)
                                                                         counter += 1
                                                                         If progress IsNot Nothing Then
                                                                             progress.Report(counter * 100 / dirsTotal)
                                                                         End If
                                                                     Next

                                                                     Return counter
                                                                 End Function)
        Return dirsFraction
    End Function


Remember, I'm still learning this myself, so I need to refer you to the earlier listed articles for an explanation as to why this returns Task(Of Integer) rather than just an integer. Roughly, an async method is unlike a normal method, whatever is returned from it is wrapped in a Task.

There isn't an easy way to just read the entire size of a folder, it requires iterating the files and subfolders (which is self-defeating). Instead, I provide a very rough estimate of how large the folder is by counting the number of its subfolders. This will at least allow us to estimate, and report, progress by ticking off each subfolder as it is iterated.



This is a poor estimate because, typically, a folder will contain one or two subfolders that are much larger than any others, and several that are very small. So, unless you choose a location that has a reasonable number of subfolders, You'll find that the ProgressBar does odd things. It might move very little, pause for a long time, then suddenly jump to 100%. (Actually, this is no worse than the behaviour of progressbars with many applications that I've seen!)

If you use C:\Windows it will probably take a long time to reach 99%, but then appear to stall. This is because it contains (in modern versions) a very large folder named WinSxS.



Dim dirsFraction As Integer = Await Task(Of Integer).Run(Function()


This is where we are awaiting the completion of a Task. We are creating our own Task, the .NET Framework has a number of async methods that we might be using instead, such as WebClient.UploadFileAsync Method, which would just be called.

The awaited Task returns an integer, which will then be assigned to dirsFraction, and subsequently returned from our method.

The anonymous function we are using is itself fairly straight-forward:
Function()
     Dim counter As Integer = 0
     For Each subDir As String In Directory.GetDirectories(location)
         SubfolderFiles(subDir)
         counter += 1
         If progress IsNot Nothing Then
             progress.Report(counter * 100 / dirsTotal)
         End If
     Next

     Return counter
 End Function


It iterates subfolders one at a time, ticking off each one with 'counter'. It Reports progress via the 'progress' delegate, passing the fraction of subfolders completed, which value is then used to update the ProgressBar.

The procedure SubfolderFiles appears further down in the code. Typically, iterating files and folders is done recursively, but recursion is not very stack friendly.



Here is a second version of the sample that uses the Cancel button and a CancellationToken to cancel iterations. You'll notice that it doesn't cancel immediately, as it only checks the token for each completed subfolder. Essentially, the task checks for a cancellation request, then throws an Exception that is caught.
Imports System.IO
Imports System.Threading

Public Class frmAsyncProgress
    Private tokenSource As CancellationTokenSource

    Private Sub frmAsyncProgress_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        barFileProgress.Minimum = 0
        barFileProgress.Maximum = 100
        btnCancel.Enabled = False
    End Sub

    Private Async Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
        If String.IsNullOrWhiteSpace(txtPath.Text) Then
            MessageBox.Show("Provide a location first.", "Location")
            Exit Sub
        End If
        Dim sLocation As String = txtPath.Text.Trim()
        If Not Directory.Exists(sLocation) Then
            MessageBox.Show("Directory doesn't exist.", "Location")
            Exit Sub
        End If

        Dim progressIndicator = New Progress(Of Integer)(AddressOf UpdateProgress)

        btnStart.Enabled = False
        btnCancel.Enabled = True
        lblPercent.Text = "0%"
        barFileProgress.Value = 0
        tokenSource = New CancellationTokenSource()

        Try
            Dim allFiles As Integer = Await AllSubfolderFiles(sLocation, progressIndicator, tokenSource.Token)
        Catch ex As OperationCanceledException
            'do stuff when cancelled
            lblPercent.Text = "Cancelled"
        End Try

        btnStart.Enabled = True
        btnCancel.Enabled = False
    End Sub

    Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click
        btnCancel.Enabled = False
        btnStart.Enabled = False
        tokenSource.Cancel()
    End Sub

    Private Async Function AllSubfolderFiles(location As String, progress As IProgress(Of Integer), _
                                             token As CancellationToken) As Task(Of Integer)
        Dim dirsTotal As Integer = Directory.GetDirectories(location).Length
        Dim dirsFraction As Integer = Await Task(Of Integer).Run(Function()
                                                                     Dim counter As Integer = 0
                                                                     For Each subDir As String In Directory.GetDirectories(location)
                                                                         SubfolderFiles(subDir)
                                                                         counter += 1
                                                                         token.ThrowIfCancellationRequested()

                                                                         If progress IsNot Nothing Then
                                                                             progress.Report(counter * 100 / dirsTotal)
                                                                         End If
                                                                     Next

                                                                     Return counter
                                                                 End Function)
        Return dirsFraction
    End Function

    Private Sub UpdateProgress(value As Integer)
        barFileProgress.Value = value
        lblPercent.Text = (value / 100).ToString("#0.##%")
    End Sub

    Private Sub SubfolderFiles(location As String)
        'source: http://stackoverflow.com/questions/16237291/visual-basic-2010-continue-on-error-unauthorizedaccessexception#answer-16237749

        Dim paths = New Queue(Of String)()
        Dim fileNames = New List(Of String)()

        paths.Enqueue(location)

        While paths.Count > 0
            Dim sDir = paths.Dequeue()

            Try
                Dim files = Directory.GetFiles(sDir)
                For Each file As String In Directory.GetFiles(sDir)
                    fileNames.Add(file)
                Next

                For Each subDir As String In Directory.GetDirectories(sDir)
                    paths.Enqueue(subDir)
                Next
            Catch ex As UnauthorizedAccessException
                ' log the exception or ignore it
                Debug.WriteLine("Directory {0}  could not be accessed!", sDir)
            Catch ex As Exception
                ' log the exception or ...
                Throw
            End Try
        End While
        'could return fileNames collection
    End Sub

End Class


You'll notice also that the ProgressBar stops (eventually), but still has an animation effect. I'll leave you to investigate if you want to stop this animation (it isn't as easy as it should be).

There is a third version in the spoiler that uses a named, rather than anonymous, Function.

Spoiler


Is This A Good Question/Topic? 1
  • +

Page 1 of 1