Hi

I've developed an app that reads in a csv file, strips it out to its component fields, validates them and writes it up to the database.

So far, so good.

I now want to enhance it so that the dataloading take place in the background enabling the UI to be used for some progress indication when loading large files.

I've managed to get the loading working in both a thread and backgroundworker but have not managed to do anything successful with the UI. I've read numerous posts on the net, which seem to make sense but I can't see how to apply them to my "real world" situation.

If I use a thread, I can't do anything with the controls on the form as they belong to another thread. When I tried the backgroundworker, the UI still appears to be frozen.

So I've stripped all that code out and my app is running on the single thread again.

I've tried to keep the code on the form to just the visual effects calling methods in another class to do the data reading, manipulation, validation and loading.

Any help would be greatly appreciated, as I just can't get it to do what I need.

Regards

Andrew

Recommended Answers

All 12 Replies

You could start the file IO in a separate class from the UI and use Thread.Start(). In your file IO class, you can raise an event after each operation (or whatever interval you choose) and have it call back to the UI to perform updates.

I think I can manage the RaiseEvent and get it to do some visuals on the form, however, how do I make it call back to th UI to perform updates on the UI.

I have already ran it in a thread and raised an event when it fininshed.

It's the callin back to the UI that escapes me at the moment.

In the code of your UI try using
AddHandler objFileIOClass.Event, AddressOf UIClass.Function
where FileIOClass.Event is the Event Raised from the FileIOClass and UIClass.Function is the Address of the function you want to execute in the UI when the Event is raised. It should look something like this:

Dim objFileIOClass as New FileIOClass

AddHandler objFileIOClass .Event, AddressOf UIClass.Function

Dim FileIOThread = New Threading.Thread(AddressOf FileIOClass.BeginOperation)

FileIOThread.Start()

commented: Thank you! Yes been a few years yet helped me with a multi-threading task :) +0

Hi

Thanks for the help so far, but I'm still struggling with this I'm afraid.

I have the "lengthy process" running in a thread that is called from a button click on the front end main form (UI).

I raise an event at the end of the "lengthy process", but the event handler wont do everything that I need it to because it errors saying the controls I am trying to update belong to another thread.

The "lengthy process" is in a seperate class from the frm.vb code.

Thanks

Andrew

That means that the thread that you started for the "lengthy process" is the thread that is trying to update the UI. Could you post the code from the button_click event?

Code for my button click event.

Private Sub btnGo1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGo1.Click

        aiidb2.ExcludeDuplicates = False

        If (txtDelim1.Text = "") Then
            MessageBox.Show("You MUST enter a field delimiter for the CSV input file")
            txtDelim1.Focus()
            GoTo lab_end
        End If

        If (txtAimsNo.Text = "") Then
            MessageBox.Show("You MUST enter an AIMS number for the input file")
            txtAimsNo.Focus()
            GoTo lab_end
        End If

        If (txtReceiptNo.Text = "") Then
            MessageBox.Show("You MUST enter a contractor receipt number for the input file")
            txtReceiptNo.Focus()
            GoTo lab_end
        End If

        If aiidb2.DuplicatesFound Then
            Dim dlgRes As DialogResult
            dlgRes = MessageBox.Show( _
                  "This contractor return contains duplicate rows." + vbCr + vbLf + _
                  "Do you want to load the duplicates into SOURCEData?", "File Contains Duplicate Rows", _
            MessageBoxButtons.YesNoCancel, _
                  MessageBoxIcon.Question)
            If dlgRes = Windows.Forms.DialogResult.Yes Then
                aiidb2.ExcludeDuplicates = False
            ElseIf dlgRes = Windows.Forms.DialogResult.No Then
                aiidb2.ExcludeDuplicates = True
            Else
                GoTo lab_end
            End If
        End If

        ' Get current date/time and format it
        aiidb2.GetDateTime()
        aiidb2.FormatDateTime()

        aiidb2.NowDate = aiidb2.Now.Year.ToString + aiidb2.NowMonth + aiidb2.NowDay
        aiidb2.NowTime = aiidb2.NowHour + aiidb2.NowMinute

        If aiidb2.ExcludeDuplicates Then
            'Create file of duplicates
            aiidb2.DuplicatesFile()
        End If

        ' ***** Initialise variables
        aiidb2.ErrorRate = 0

        'Test if replayed error file
        If (aiidb2.ErrorFile) Then
            If MessageBox.Show("This is an error file." + vbCr + vbLf + _
            "Has it been corrected?" + vbCr + vbLf + _
            "Are you sure you want to replay this error file?", "Error File", _
            MessageBoxButtons.YesNo, MessageBoxIcon.Question) _
            = Windows.Forms.DialogResult.Yes Then
                'do Nothing
            Else
                GoTo lab_end
            End If
        End If

        aiidb2.CreateOriginalFileName()

        Me.Cursor = Cursors.WaitCursor

        ' Call to the lengthy process here
        'Dim t As New Thread(AddressOf aiidb2.LoadData)
        'AddHandler aiidb2.FinishedDataLoading, AddressOf FinishedDataLoadingEventHandler
        't.Start()

lab_end:

    End Sub

THis is the code I'm trying to run in the event handler when the lengthy process finishes. It fails when I try to do anything with the cursor or labels/ textboxes / buttons etc. on the form.

'If no errors, tidy files up
        If (aiidb2.ErrorCount = 0) Then
            'Archive orignal file here
            File.Move(aiidb2.FilePath + aiidb2.OriginalFileName, aiidb2.FilePathArchived + aiidb2.OriginalFileName)

            ' TODO: Possibly set DataLoadRequired flag on DB to false after successful load

            ' TODO: Business decision on whether to delete replayed / corrected errorfile or Archive
            ' Currently archive the file

            ' ***** DELETE ErrorFile *****
            'Delete replayed error file (this can be reintroduced if neccessary, decided to keep corrected error file)
            'If (aiidb2.errorFile) Then
            'File.Delete(filePath + FileName)
            'End If

            ' ***** ARCHIVE ErrorFile *****
            'Archive replayed / corrected errorfile
            If (aiidb2.ErrorFile) Then
                File.Move(aiidb2.FileName, aiidb2.FilePathArchived + aiidb2.FileNameNoPath)
            End If

            'MessageBox.Show("Original RAW data archived & any replayed error file deleted")
        End If

        'Calculate error rate as percentage
        aiidb2.ErrorRate = Math.Round(((aiidb2.ErrorCount / aiidb2.EndCount) * 100), 2)

        Me.Cursor = Cursors.Default

        If (aiidb2.DuplicatesFound And aiidb2.ExcludeDuplicates) Then
            MessageBox.Show(aiidb2.SuccessCount & " Rows Inserted into SOURCEData" & vbCr & vbLf & vbCr & vbLf & aiidb2.Dups.Count.ToString & " Rows written to DUPLICATES file" & vbCr & vbLf & vbCr & vbLf & aiidb2.ErrorCount & " Rows written to ERROR file" & vbCr & vbLf & vbCr & vbLf & "Error rate: " & aiidb2.ErrorRate & "%", "Load Summary")
        Else
            MessageBox.Show(aiidb2.SuccessCount & " Rows Inserted into SOURCEData" & vbCr & vbLf & vbCr & vbLf & aiidb2.ErrorCount & " Rows written to ERROR file" & vbCr & vbLf & vbCr & vbLf & "Error rate: " & aiidb2.ErrorRate & "%", "Load Summary")
        End If

        lblFilename1.Visible = False
        lblFilename1.Text = Nothing
        aiidb2.FileName = Nothing
        lblDelim1.Visible = False
        lblAimsNo.Visible = False
        lblReceiptNo.Visible = False
        txtDelim1.Visible = False
        txtAimsNo.Text = ""
        txtAimsNo.Visible = False
        txtReceiptNo.Text = ""
        txtReceiptNo.Visible = False
        btnBrowseData.Visible = False
        btnGo1.Visible = False
        panData.Visible = False

Thanks

Andrew

Hi

Just noticed that the call to my lengthy process in the button click event is commented out. Obviously this wasn't the case when I was testing it.

Thanks

Andrew

Apparently, I have given you bad advice. The solution I presented appears to no longer be supported. The following is a comprehensive view of updating a UI from another thread and also using the background worker. This is an actual program that demonstrates the concepts. You can start a new Windows Application Project and paste the code in the code-behind of Form1. If you get build errors, delete the code from the Form1.Designer.vb file.

Imports System
Imports System.ComponentModel
Imports System.Threading
Imports System.Windows.Forms

Public Class Form1
   Inherits Form

   ' This delegate enables asynchronous calls for setting
   ' the text property on a TextBox control.
   Delegate Sub SetTextCallback([text] As String)

   ' This thread is used to demonstrate both thread-safe and
   ' unsafe ways to call a Windows Forms control.
   Private demoThread As Thread = Nothing

   ' This BackgroundWorker is used to demonstrate the 
   ' preferred way of performing asynchronous operations.
   Private WithEvents backgroundWorker1 As BackgroundWorker

   Private textBox1 As TextBox
   Private WithEvents setTextUnsafeBtn As Button
   Private WithEvents setTextSafeBtn As Button
   Private WithEvents setTextBackgroundWorkerBtn As Button

   Private components As System.ComponentModel.IContainer = Nothing


   Public Sub New()
      InitializeComponent()
    End Sub


   Protected Overrides Sub Dispose(disposing As Boolean)
      If disposing AndAlso (components IsNot Nothing) Then
         components.Dispose()
      End If
      MyBase.Dispose(disposing)
    End Sub


   ' This event handler creates a thread that calls a 
   ' Windows Forms control in an unsafe way.
    Private Sub setTextUnsafeBtn_Click( _
    ByVal sender As Object, _
    ByVal e As EventArgs) Handles setTextUnsafeBtn.Click

        Me.demoThread = New Thread( _
        New ThreadStart(AddressOf Me.ThreadProcUnsafe))

        Me.demoThread.Start()
    End Sub


   ' This method is executed on the worker thread and makes
   ' an unsafe call on the TextBox control.
   Private Sub ThreadProcUnsafe()
      Me.textBox1.Text = "This text was set unsafely."
   End Sub 

   ' This event handler creates a thread that calls a 
   ' Windows Forms control in a thread-safe way.
    Private Sub setTextSafeBtn_Click( _
    ByVal sender As Object, _
    ByVal e As EventArgs) Handles setTextSafeBtn.Click

        Me.demoThread = New Thread( _
        New ThreadStart(AddressOf Me.ThreadProcSafe))

        Me.demoThread.Start()
    End Sub


   ' This method is executed on the worker thread and makes
   ' a thread-safe call on the TextBox control.
   Private Sub ThreadProcSafe()
      Me.SetText("This text was set safely.")
    End Sub

   ' This method demonstrates a pattern for making thread-safe
   ' calls on a Windows Forms control. 
   '
   ' If the calling thread is different from the thread that
   ' created the TextBox control, this method creates a
   ' SetTextCallback and calls itself asynchronously using the
   ' Invoke method.
   '
   ' If the calling thread is the same as the thread that created
    ' the TextBox control, the Text property is set directly. 

    Private Sub SetText(ByVal [text] As String)

        ' InvokeRequired required compares the thread ID of the
        ' calling thread to the thread ID of the creating thread.
        ' If these threads are different, it returns true.
        If Me.textBox1.InvokeRequired Then
            Dim d As New SetTextCallback(AddressOf SetText)
            Me.Invoke(d, New Object() {[text]})
        Else
            Me.textBox1.Text = [text]
        End If
    End Sub

   ' This event handler starts the form's 
   ' BackgroundWorker by calling RunWorkerAsync.
   '
   ' The Text property of the TextBox control is set
   ' when the BackgroundWorker raises the RunWorkerCompleted
   ' event.
    Private Sub setTextBackgroundWorkerBtn_Click( _
    ByVal sender As Object, _
    ByVal e As EventArgs) Handles setTextBackgroundWorkerBtn.Click
        Me.backgroundWorker1.RunWorkerAsync()
    End Sub


   ' This event handler sets the Text property of the TextBox
   ' control. It is called on the thread that created the 
   ' TextBox control, so the call is thread-safe.
   '
   ' BackgroundWorker is the preferred way to perform asynchronous
   ' operations.
    Private Sub backgroundWorker1_RunWorkerCompleted( _
    ByVal sender As Object, _
    ByVal e As RunWorkerCompletedEventArgs) _
    Handles backgroundWorker1.RunWorkerCompleted
        Me.textBox1.Text = _
        "This text was set safely by BackgroundWorker."
    End Sub

   #Region "Windows Form Designer generated code"


   Private Sub InitializeComponent()
      Me.textBox1 = New System.Windows.Forms.TextBox()
      Me.setTextUnsafeBtn = New System.Windows.Forms.Button()
      Me.setTextSafeBtn = New System.Windows.Forms.Button()
      Me.setTextBackgroundWorkerBtn = New System.Windows.Forms.Button()
      Me.backgroundWorker1 = New System.ComponentModel.BackgroundWorker()
      Me.SuspendLayout()
      ' 
      ' textBox1
      ' 
      Me.textBox1.Location = New System.Drawing.Point(12, 12)
      Me.textBox1.Name = "textBox1"
      Me.textBox1.Size = New System.Drawing.Size(240, 20)
      Me.textBox1.TabIndex = 0
      ' 
      ' setTextUnsafeBtn
      ' 
      Me.setTextUnsafeBtn.Location = New System.Drawing.Point(15, 55)
      Me.setTextUnsafeBtn.Name = "setTextUnsafeBtn"
      Me.setTextUnsafeBtn.TabIndex = 1
      Me.setTextUnsafeBtn.Text = "Unsafe Call"
      ' 
      ' setTextSafeBtn
      ' 
      Me.setTextSafeBtn.Location = New System.Drawing.Point(96, 55)
      Me.setTextSafeBtn.Name = "setTextSafeBtn"
      Me.setTextSafeBtn.TabIndex = 2
      Me.setTextSafeBtn.Text = "Safe Call"
      ' 
      ' setTextBackgroundWorkerBtn
      ' 
      Me.setTextBackgroundWorkerBtn.Location = New System.Drawing.Point(177, 55)
      Me.setTextBackgroundWorkerBtn.Name = "setTextBackgroundWorkerBtn"
      Me.setTextBackgroundWorkerBtn.TabIndex = 3
      Me.setTextBackgroundWorkerBtn.Text = "Safe BW Call"
      ' 
      ' backgroundWorker1
      ' 
      ' 
      ' Form1
      ' 
      Me.ClientSize = New System.Drawing.Size(268, 96)
      Me.Controls.Add(setTextBackgroundWorkerBtn)
      Me.Controls.Add(setTextSafeBtn)
      Me.Controls.Add(setTextUnsafeBtn)
      Me.Controls.Add(textBox1)
      Me.Name = "Form1"
      Me.Text = "Form1"
      Me.ResumeLayout(False)
      Me.PerformLayout()
   End Sub 'InitializeComponent 

   #End Region

   <STAThread()>  _
   Shared Sub Main()
      Application.EnableVisualStyles()
      Application.Run(New Form1())
    End Sub
End Class

Tried that.

Don't have a clue what it's doing.

Just don't understand this concept whatsoever.

Ok,
What you want to do is create a Delegate Sub like this:

Delegate Sub LengthyProcessCallback()

In the beginning of the code for the event handler function add this:

'Test to see if the correct thread is being used. If not, InvokeRequired will be true.

  If Me.lblFilename1.InvokeRequired Then
            'If InvokeRequired = True then this function will be re-called from the UI Thread.
            Dim d As New LengthyProcessCallback(AddressOf [I]ThisFunction[/I])
            Me.Invoke(d)
        Else
            [I]Continue normal processing[/I]
        End If

I hope this makes more sense to you in the context of your app.

Could yoo expand on that a little more. Sorry, I'm being realy dense with this.

Where doe the Delegate Sub....... need toi go in the code?

And the remainder of the code you have listed, where does that go and what do I need to replace ThisFunction with?

Incidently, my lengthy process is called LoadData and I tried raining the event FinishedDataLoading. But that's as far I got and then I had trouble with

Do you have a simple example?

Thanks for your help.

Andrew

The Delegate Sub can be put anywhere within the Class declaration, but not within another Sub or Function.

The name of the EventHandler Function should replace ThisFunction. In other words,
the name of the function that is being called when the Event is being raised.

The remainder of the code would go into the beginning of the EventHandler function like this:

Public Sub EventHandler()
'Test to see if the correct thread is being used. If not, InvokeRequired will be true.

  If Me.lblFilename1.InvokeRequired Then
            'If InvokeRequired = True then this function will be re-called from the UI Thread.
            Dim d As New LengthyProcessCallback(AddressOf ThisFunction)
            Me.Invoke(d)
        Else
         [I]Place your code here from the EventHandler Function[/I]  
End If
End Sub
commented: Thanks! This worked :) +0
Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.