0

I have a Windows Form, that has a Telerik RadGridView with a bunch of records to be imported into our SQL database. The RadGridView just shows the users the records that are going to be imported, in case they see an easy to fix mistake that they could address, before importing the records.

I have the following controls on the Winform form:

rgImport - Telerik RadGridView control
btnSubmit - button control
ProgressBar1 - Progress Bar for when importing the records
Label1 - some text telling the user to review the records presented in rgImport

I am looking to make some of these functions Async/Await, so the UI doesn't get locked, and the progress bar shows what percentage of records have been imported:

The two calls to these functions:
DataAPI.HR_Payroll_TimeCards_Insert(dr)
DataAPI.HR_Payroll_TimeCards_Insert_Double(dr)
do nothing more than inject the records into our database server tables and return a boolean on whether is was successful or failed

Private Sub Import_Payroll_Review_Load(sender As Object, e As EventArgs) Handles Me.Load
        rgImport.DataSource = DataAPI.HR_Payroll_Import_GetData()
        ProgressBar1.Minimum = 0
        ProgressBar1.Maximum = 100
        ProgressBar1.Visible = False
End Sub

DataAPI.HR_Payroll_Import_GetData() - just returns a DataTable that is used to load up the RadGridView with records

Private Sub btnSubmit_Click(sender As Object, e As EventArgs) Handles btnSubmit.Click
   
   Select Case MsgBox("Are you sure you are ready to import?", MsgBoxStyle.YesNo, "Import Records")
      Case MsgBoxResult.Yes

        'Import the records from the import table
        Dim dt As DataTable = DataAPI.HR_Payroll_Import_GetRecordsToImport()
        Dim iRows As Long = 1
        Dim iTotalRows As Long = dt.Rows.Count

        'CallTheInsertMethodHere

        For Each dr As DataRow In dt.Rows

            If dr("AfterHours") = 0 Then
                'This is standard time, so import it into the [TimeCards.Current] table
                If DataAPI.HR_Payroll_TimeCards_Insert(dr) Then
                    'Success
                    Debug.Print("Inserted Row into TimeCard.Current")
                Else
                    'Failed
                    Debug.Print("Failed to insert row into TimeCard.Current")
                End If
            Else
                'This is double time, so import it into the [TimeCards.Current.Double] table
                If DataAPI.HR_Payroll_TimeCards_Insert_Double(dr) Then
                    'Success
                    Debug.Print("Inserted Row into TimeCard.Current.Double")
                Else
                    'Failed
                    Debug.Print("Failed to insert row into TimeCard.Current.Double")
                End If
            End If

            iRows += 1

        Next
        MsgBox("Inserted " & iRows - 1 & " rows", vbInformation, "Success")

    Case Else
        'User clicked on no, so exit
        Exit Sub
  End Select
End Sub

I am just looking for a little help/direction on how to break this up and where to add Async/Await to allow the UI to be free'd up to show what percentage of the records have been inserted using the ProgressBar (The total records is in the variable iTotalRows)

Thank you for your time reviewing this and any input you may provide to me to get this working.

djv
  • 15,168
  • 7
  • 48
  • 72
  • 1
    That depends on what `DataAPI.HR_Payroll_Import_GetData()`, `DataAPI.HR_Payroll_Import_GetRecordsToImport()`, `DataAPI.HR_Payroll_TimeCards_Insert()` and `DataAPI.HR_Payroll_TimeCards_Insert_Double(dr)` are doing. If you're using simple OleDb / SqlClient Connection + DataReader, all these have async versions of their methods, so you can always `await` / `return await` their results. The same goes for the loop, if the methods are awaitable. -- Otherwise, you can Task.Run() the whole procedure(s) and pass an `IProgress` delegate to the Tasks(s) to update the UI. – Jimi Sep 23 '21 at 20:07
  • Of course, you need to prevent the User from interacting with *sensible* UI elements while the async procedures are running (e.g., you need to disable that Button. Also applies to closing the Form in the meanwhile). Which also implies that you may need to pass a CancellationToken to the Tasks (or create a default one), in case the User wants to terminate the operation(s) and/or you want to set a timeout (using a CancellationToken with a timeout set). – Jimi Sep 23 '21 at 20:10
  • See a few related (simplified) examples here: [Start a Task in the Form Shown event](https://stackoverflow.com/a/60571698/7444103) – Jimi Sep 23 '21 at 20:14
  • Jimi, thanks for the quick response and information. I am looking at the example you sent now. Will report back. – Eric Lindros Sep 23 '21 at 20:29
  • Another option, what I'm seeing here seem well suited to the slightly outdated backgroundworker pattern instead of await/async. – Hursey Sep 23 '21 at 20:56
  • Hursey - thank you. That may actually be a better idea. I'm also reading up on that. Thank you. – Eric Lindros Sep 23 '21 at 21:18

1 Answers1

0

This is a basic implementation of Async/Await for you to see how the UI is still responsive while Awaiting DoWorkAsync, but not while running DoWork. I included progress from this Stephen Cleary answer

Private Function DoWorkAsync(progress As IProgress(Of Integer)) As Task(Of Boolean)
    Return Task.Run(Function() DoWork(progress))
End Function

Private Function DoWork(progress As IProgress(Of Integer)) As Boolean
    Dim i As Integer
    While i < 100
        i += (New Random).Next(5)
        Threading.Thread.Sleep(50)
        progress.Report(i)
    End While
    Return True
End Function

Private Async Sub btnSubmit_Click(sender As Object, e As EventArgs) Handles btnSubmit.Click
    Dim progress = New Progress(Of Integer)(
        Sub(value)
            ProgressBar1.Value = Math.Max(Math.Min(value, ProgressBar1.Maximum), ProgressBar1.Minimum)
            Console.WriteLine(value)
        End Sub)
    If MessageBox.Show("Run Async?", "Confirm", MessageBoxButtons.YesNo) = DialogResult.Yes Then
        Await DoWorkAsync(progress)
    Else
        DoWork(progress)
    End If
    MessageBox.Show("Done")
End Sub

You can basically put all your work into DoWork, with success logic (that's why I returned Boolean) and it won't block the UI. I wouldn't worry about making separate functions for DataAPI.HR_Payroll_Import_GetData(), DataAPI.HR_Payroll_Import_GetRecordsToImport(), DataAPI.HR_Payroll_TimeCards_Insert() or DataAPI.HR_Payroll_TimeCards_Insert_Double(dr) unless you need that granularity for other parts of your code.

You can add a CancellationToken, and with your own code doing the work, it might look like this

Private Function DoWorkAsync(progress As IProgress(Of Integer), cancellationToken As Threading.CancellationToken) As Task(Of Boolean)
    Return Task.Run(Function() DoWork(progress, cancellationToken))
End Function

Private Function DoWork(progress As IProgress(Of Integer), cancellationToken As Threading.CancellationToken) As Boolean
    Try
        'Import the records from the import table
        Dim dt As DataTable = DataAPI.HR_Payroll_Import_GetRecordsToImport()
        Dim iRows As Long = 1
        Dim iTotalRows As Long = dt.Rows.Count
        'CallTheInsertMethodHere
        For Each dr As DataRow In dt.Rows
            If dr("AfterHours") = 0 Then
                'This is standard time, so import it into the [TimeCards.Current] table
                If DataAPI.HR_Payroll_TimeCards_Insert(dr) Then
                    'Success
                    Debug.Print("Inserted Row into TimeCard.Current")
                Else
                    'Failed
                    Debug.Print("Failed to insert row into TimeCard.Current")
                End If
            Else
                'This is double time, so import it into the [TimeCards.Current.Double] table
                If DataAPI.HR_Payroll_TimeCards_Insert_Double(dr) Then
                    'Success
                    Debug.Print("Inserted Row into TimeCard.Current.Double")
                Else
                    'Failed
                    Debug.Print("Failed to insert row into TimeCard.Current.Double")
                End If
            End If
            iRows += 1
            progress.Report(CInt(100 * iRows / iTotalRows))
            cancellationToken.ThrowIfCancellationRequested()
        Next
        Return True
    Catch ex As Exception
        Debug.Print(ex.Message)
        Return False
    End Try
End Function

Private Async Sub btnSubmit_Click(sender As Object, e As EventArgs) Handles btnSubmit.Click
    Dim progress As IProgress(Of Integer) = New Progress(Of Integer)(
    Sub(value)
        ProgressBar1.Value = Math.Max(Math.Min(value, ProgressBar1.Maximum), ProgressBar1.Minimum)
        Console.WriteLine(value)
    End Sub)
    cancellationSource = New Threading.CancellationTokenSource()
    Dim cancellationToken = cancellationSource.Token
    cancellationToken.Register(Sub() progress.Report(0))
    If MessageBox.Show("Run Async?", "Confirm", MessageBoxButtons.YesNo) = DialogResult.Yes Then
        Await DoWorkAsync(progress, cancellationToken)
    Else
        DoWork(progress, cancellationToken)
    End If
    progress.Report(0)
    If cancellationToken.IsCancellationRequested Then
        MessageBox.Show("Cancelled")
    Else
        MessageBox.Show("Done")
    End If
End Sub

Private cancellationSource As New Threading.CancellationTokenSource()

Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click
    cancellationSource.Cancel()
End Sub

Credit to Stephen Cleary for the CancellationToken, again

djv
  • 15,168
  • 7
  • 48
  • 72
  • Thank you for responding. I will spend some time today digesting all of this and report back how it goes. Much thank you for taking the time!! – Eric Lindros Sep 24 '21 at 12:38