• Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 564
  • Last Modified:

Running a background thread from my form in .Net

I don't know why I struggle with bgw's but I do...
I am working on a project in visual studio that will allow me to basically keep a food journal.
I've got the code that reads in nutritional info from a web site, but I want to move it to a code module under a bgw so that I can have the form display a progress bar while it's reading that data.  
So, how can I accomplish this?  Or am I doing too much?  The data read only takes a few moments, but it's enough lag that I need 'something' to signify it's working.

My form simply calls RunWorker sub passing in the food description.

My module contains the following code, initiated with a
WithEvents bgw as backgroundworker


Public Sub RunWorker(ByVal f As Food)
        Dim bgw As New BackgroundWorker
        AddHandler bgw.DoWork, AddressOf bgwWork
        AddHandler bgw.RunWorkerCompleted, AddressOf bgwDone
        bgw.RunWorkerAsync(f)
    End Sub
 
    Private Sub bgwWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles bgw.DoWork
        'Populate form's combo box with results
    End Sub
 
    Private Sub bgwDone(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) Handles bgw.RunWorkerCompleted
        Stop
    End Sub
 
    Public Class Food
        Public Description As String
        Public Calories, Fat, Carbs, FatCal, Protein As Integer
 
        Public Sub New(ByVal strDesc As String)
            Me.Description = strDesc
        End Sub
    End Class

Open in new window

0
sirbounty
Asked:
sirbounty
  • 15
  • 6
  • 4
  • +1
1 Solution
 
openshacCommented:
From bgwWork call the Progress event (you will need to enable this at design time).

The Progress event runs on the GUI thread so you can display something to show that the task is running. Then run the task (asynchronously)

In the Completed event you can reset the appearance of the GUI.
0
 
mastooCommented:
There's a good example in the visual studio help under BackgroundWorker class.  The key thing your code would have a problem with is:

You must be careful not to manipulate any user-interface objects in your DoWork event handler. Instead, communicate to the user interface through the ProgressChanged and RunWorkerCompleted events.

I might suggest also if it is just a short delay you're working with, it can be acceptable to just use a single thread on the form and have intersperse a few status updates in the "lengthy" code to let the gui indicate something is working.
0
 
sirbountyAuthor Commented:
If I were to just run this on the form, w/o the bgw, I suppose I need something to allow the form to be updated?  DoEvents?  If so, where do I place it?
This is my code to search the results...
     Dim myFood As New Food(txtFood.Text.Trim)
 
        pbStatus.Visible = True
 
        Dim wc As New System.Net.WebClient()
        Dim s As System.IO.Stream
        Dim sr As System.IO.StreamReader
        pbStatus.Value = 20
        s = wc.OpenRead("http://www.thedailyplate.com/nutrition/search.php?q=" & txtFood.Text.Replace(" ", "+"))
        pbStatus.Value = 50
        sr = New System.IO.StreamReader(s)
        Dim r As String = sr.ReadToEnd()
        Dim regex As RegularExpressions.Regex = Nothing
        Dim result As String = regex.Replace(r, "<[^>]*>", String.Empty)
        result = result.Substring(result.IndexOf("&nbsp;", result.IndexOf("search_query")) + 6).Trim.Replace("&nbsp;", "|")
        result = result.Substring(0, result.IndexOf("num_ads") - 8)
        Dim arrData() As String = result.Split("|")
 
        For x As Int16 = 0 To arrData.Length - 1
            Dim arrItems() As String = arrData(x).Split(vbLf)
            Dim sb As New StringBuilder
            For Each item As String In arrItems
                If item.Trim <> "" Then
                    If item.IndexOf("erving") = -1 Then
                        sb.Append(item.Trim & " ")
                    Else
                        item = item.Trim
                        sb.Append(item.Substring(item.IndexOf("Calories:")))
                    End If
                End If
            Next
            cboSearchResults.Items.Add(sb.ToString)
        Next
 
        cboSearchResults.Visible = True
 
        '--tidy up
        sr.Close()
        s.Close()

Open in new window

0
Prepare for your VMware VCP6-DCV exam.

Josh Coen and Jason Langer have prepared the latest edition of VCP study guide. Both authors have been working in the IT field for more than a decade, and both hold VMware certifications. This 163-page guide covers all 10 of the exam blueprint sections.

 
openshacCommented:
There's no point calling it until the combo is visible which happens at:
cboSearchResults.Visible = True
so pop it after this.

Personally I would use the BackGroundWorker object, all the code you need is already custom built, they just work and are thread safe.

0
 
sirbountyAuthor Commented:
Ok, so I think I can do this:
"From bgwWork call the Progress event (you will need to enable this at design time)."
but how do I reference the bgw referenced in my module?  Should I set the withevents to public?
0
 
openshacCommented:
Just add the backgroundworker control to your form.
go to properties and set WorkerSupportsProgress to True
Then create the events for Dowork, ProgressChanged, RunWorkerCompleted
0
 
sirbountyAuthor Commented:
So you're saying my bgw has to be on my form - not my module..?
0
 
openshacCommented:
Sorry I don't know how you app is structured.
You don't need it on your form, no, just thought that if you had a form that would be easiest way to get all the code in place.  I would recommend creating an empty Windows Application and putting one on the form, hook up the events and then you have all the code that you need to put into your module.

If your BW object is not on the form you will need to tell it which thread the GUI is on so in your ProgressChanged, and RunWorkerCompleted method will use use

Invoke(new MethodInvoker(delegate
{
//your code to update the ui
}));
0
 
sirbountyAuthor Commented:
This is all I have in reference on my form:

        RunWorker(myFood)

        While bgw.IsBusy
            If pbStatus.Value < pbStatus.Maximum Then
                pbStatus.Value = pbStatus.Value + 1
            End If
        End While

I've tried to make it available from the module by using:
 Public WithEvents bgw As BackgroundWorker
but I get the object reference error...

Here's my working and completed routines.
Just want to know how to determine if it's still busy and update the progress bar a bit until it's not...
   Private Sub bgwWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles bgw.DoWork
        Dim strFood As String = e.Argument.ToString.Replace(" ", "+")
 
        Dim s As Stream
        Dim sr As StreamReader
        Dim sbl As New List(Of StringBuilder)
 
        Using wc As New Net.WebClient
            s = wc.OpenRead("http://www.thedailyplate.com/nutrition/search.php?q=" & strFood)
        End Using
 
        sr = New StreamReader(s)
        Dim r As String = sr.ReadToEnd()
        Dim regex As RegularExpressions.Regex = Nothing
        Dim result As String = regex.Replace(r, "<[^>]*>", String.Empty)
        result = result.Substring(result.IndexOf("&nbsp;", result.IndexOf("search_query")) + 6).Trim.Replace("&nbsp;", "|")
        result = result.Substring(0, result.IndexOf("num_ads") - 8)
        Dim arrData() As String = result.Split("|")
 
        For x As Int16 = 0 To arrData.Length - 1
            Dim arrItems() As String = arrData(x).Split(vbLf)
            Dim sb As New StringBuilder
            For Each item As String In arrItems
                If item.Trim <> "" Then
                    If item.IndexOf("erving") = -1 Then
                        sb.Append(item.Trim & " ")
                    Else
                        item = item.Trim
                        sb.Append(item.Substring(item.IndexOf("Calories:")))
                    End If
                End If
            Next
            sbl.Add(sb)
 
        Next
        'cboSearchResults.Items.Add(sb.ToString)
        e.Result = sbl
 
        '--tidy up
        sr.Close()
        s.Close()
    End Sub
 
    Private Sub bgwDone(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) Handles bgw.RunWorkerCompleted
        Dim sbl As List(Of StringBuilder) = e.Result
        For Each sb As StringBuilder In sbl
            frmMain.cboSearchResults.Items.Add(sb.ToString)
        Next
    End Sub

Open in new window

0
 
sirbountyAuthor Commented:
Think I found my problem - I was 're' declaring bgw in my sub...
0
 
sirbountyAuthor Commented:
Yet even with the bgw running - my progress bar doesn't appear /doesn't update...

And stepping through this - it appears to pass the food to the bgw at least twice?  Any ideas?
0
 
openshacCommented:
You need to call the ProgressChanged to update the UI

There's an example here:
http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx
0
 
sirbountyAuthor Commented:
doesn't explain why it'd be running that code twice?
0
 
sirbountyAuthor Commented:
It's almost as if it's hitting the dowork again when it should be hitting the 'done'...  : \
0
 
sirbountyAuthor Commented:
I've got to be missing something obvious...even though my goal is to have this in a module, I've moved it all to my form code.
No progress bar, the dowork runs twice, the bgwprogress nor bgwdone are ever hit from what I can tell, and the form just 'hangs' after the second do_work...

Here's my code:
    Private Sub btnSearch_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSearch.Click
        If txtFood.Text.Trim = "" Then Exit Sub
        cboSearchResults.Items.Clear()
        Dim myFood As New Food(txtFood.Text.Trim)
        pbStatus.Visible = True
 
        bgw = New BackgroundWorker
        bgw.WorkerReportsProgress = True
        bgw.WorkerSupportsCancellation = True
        AddHandler bgw.DoWork, AddressOf bgwWork
        AddHandler bgw.ProgressChanged, AddressOf bgw_ProgressChanged
        AddHandler bgw.RunWorkerCompleted, AddressOf bgwDone
        bgw.RunWorkerAsync(myFood.Description)
 
        While bgw.IsBusy
            If pbStatus.Value < pbStatus.Maximum Then
                pbStatus.Value = pbStatus.Value + 1
            End If
        End While
 
        pbStatus.Visible = False
        cboSearchResults.Visible = True
 
    End Sub
 
 
 
    Private Sub bgwWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles bgw.DoWork
        Dim strFood As String = e.Argument.ToString
 
        Dim s As Stream
        Dim sr As StreamReader
        Dim sbl As New List(Of StringBuilder)
        bgw.ReportProgress(10)
 
        Using wc As New Net.WebClient
            s = wc.OpenRead("http://www.thedailyplate.com/nutrition/search.php?q=" & strFood)
        End Using
        bgw.ReportProgress(50)
        sr = New StreamReader(s)
        Dim r As String = sr.ReadToEnd()
        Dim regex As RegularExpressions.Regex = Nothing
        Dim result As String = regex.Replace(r, "<[^>]*>", String.Empty)
        result = result.Substring(result.IndexOf("&nbsp;", result.IndexOf("search_query")) + 6).Trim.Replace("&nbsp;", "|")
        result = result.Substring(0, result.IndexOf("num_ads") - 8)
        Dim arrData() As String = result.Split("|")
        bgw.ReportProgress(75)
        For x As Int16 = 0 To arrData.Length - 1
            Dim arrItems() As String = arrData(x).Split(vbLf)
            Dim sb As New StringBuilder
            For Each item As String In arrItems
                If item.Trim <> "" Then
                    If item.IndexOf("erving") = -1 Then
                        sb.Append(item.Trim & " ")
                    Else
                        item = item.Trim
                        sb.Append(item.Substring(item.IndexOf("Calories:")))
                    End If
                End If
            Next
            sbl.Add(sb)
 
        Next
        bgw.ReportProgress(90)
        'cboSearchResults.Items.Add(sb.ToString)
        sr.Close()
        s.Close()
        e.Result = sbl
 
    End Sub
 
    Private Sub bgw_ProgressChanged(ByVal sender As Object, ByVal e As ProgressChangedEventArgs) Handles bgw.ProgressChanged
        pbStatus.Value = e.ProgressPercentage
    End Sub
 
    Private Sub bgwDone(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) ' Handles bgw.RunWorkerCompleted
        Dim sbl As List(Of StringBuilder) = e.Result
        For Each sb As StringBuilder In sbl
            cboSearchResults.Items.Add(sb.ToString)
        Next
    End Sub

Open in new window

0
 
Fernando SotoCommented:
Hi sirbounty;

I have modified your code to work with a module with the background worker and to report progress to the main form progress bar.

Fernando
'Code in main form
Imports System.ComponentModel
 
Public Class frmMain
 
    Private WithEvents bgw As BackgroundWorker
 
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim myFood As String = "Apple|Pare"
 
        bgw = New BackgroundWorker()
        bgw.WorkerReportsProgress = True
        AddHandler bgw.DoWork, AddressOf BgwModule.bgwWork
        AddHandler bgw.RunWorkerCompleted, AddressOf BgwModule.bgwDone
        AddHandler bgw.ProgressChanged, AddressOf BgwModule.bwgProgressChanged
 
        bgw.RunWorkerAsync(myFood)
 
    End Sub
End Class
 
==========================================================================
 
' Code in module
 
Imports System.ComponentModel
Imports System.IO
Imports System.Text
Imports System.Text.RegularExpressions
 
Module BgwModule
 
    Public Sub bgwWork(ByVal sender As Object, ByVal e As DoWorkEventArgs)
 
        Dim bgwSource As BackgroundWorker = CType(sender, BackgroundWorker)
        Dim strFood As String = e.Argument.ToString.Replace(" ", "+")
 
        Dim s As Stream
        Dim sr As StreamReader
        Dim sbl As New List(Of StringBuilder)
 
        Using wc As New Net.WebClient
            s = wc.OpenRead("http://www.thedailyplate.com/nutrition/search.php?q=" & strFood)
        End Using
 
        sr = New StreamReader(s)
        Dim r As String = sr.ReadToEnd()
        Dim regex As RegularExpressions.Regex = Nothing
        Dim result As String = regex.Replace(r, "<[^>]*>", String.Empty)
        result = result.Substring(result.IndexOf("&nbsp;", result.IndexOf("search_query")) + 6).Trim.Replace("&nbsp;", "|")
        result = result.Substring(0, result.IndexOf("num_ads") - 8)
        Dim arrData() As String = result.Split("|"c)
 
        ' Initialize the ProgressBar on the frmMain
        Dim args() As String = {"Initialize", arrData.Length.ToString()}
        bgwSource.ReportProgress(0, args)
 
        For x As Int16 = 0 To CShort(arrData.Length - 1)
            Dim arrItems() As String = arrData(x).Split(CChar(vbLf))
            Dim sb As New StringBuilder
            For Each item As String In arrItems
                If item.Trim <> "" Then
                    If item.IndexOf("erving") = -1 Then
                        sb.Append(item.Trim & " ")
                    Else
                        item = item.Trim
                        sb.Append(item.Substring(item.IndexOf("Calories:")))
                    End If
                End If
            Next
            sbl.Add(sb)
            ' Update the progress bar
            bgwSource.ReportProgress(1, New String() {"", ""})
        Next
        'cboSearchResults.Items.Add(sb.ToString)
        e.Result = sbl
 
        '--tidy up
        sr.Close()
        s.Close()
 
    End Sub
 
    Public Sub bgwDone(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
 
        Dim sbl As List(Of StringBuilder) = CType(e.Result, List(Of StringBuilder))
        For Each sb As StringBuilder In sbl
            frmMain.cboSearchResults.Items.Add(sb.ToString)
        Next
 
    End Sub
 
    Public Sub bwgProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs)
 
        ' Get the array of strings and if first element is Initialize then Initialize the progress bar
        Dim args() As String = CType(e.UserState, String())
        If args(0) = "Initialize" Then
            frmMain.ProgressBar1.Minimum = 0
            frmMain.ProgressBar1.Maximum = Integer.Parse(args(1))
            Return
        End If
 
        ' Increment the progress bar value
        frmMain.ProgressBar1.Increment(e.ProgressPercentage)
 
    End Sub
 
End Module

Open in new window

0
 
sirbountyAuthor Commented:
Hi Fernando - This works great, with one minor item - when search is pressed, I end up waiting about half the time for the progress bar to even begin filling.
Rough counting was 7 seconds before it started filling the progress bar - and then nearly 1 - 2 seconds to go from 0 to 100.

So, I tried marking it at 20 in the button event, and even moving the initialize earlier on, but again it sits there at the 20 point mark and 'hangs' for the next 5 seconds - then shoots immediately up to full.  

The 'main' event I'm waiting on is the web data being read at
  s = wc.OpenRead("http://www.thedailyplate.com/nutrition/search.php?q=" & strFood)

Is there a way to have it slowly increment until this is completely read?
If this constitutes a separate question - I have no problem opening a new one...

Thanx for your help!
0
 
Fernando SotoCommented:
Hi sirbounty;

In the code I posted these lines

' Initialize the ProgressBar on the frmMain
Dim args() As String = {"Initialize", arrData.Length.ToString()}
bgwSource.ReportProgress(0, args)


will create an array of strings with the first element set to Initialize and the second element as the total number of elements in the array arrData as a string. It is then sent to the BackgroundWorker ReportProgress event which has direct access to the progress bar. Then in the ReportProgress event the progress bar is initialized with the values passed to it in the args array.

This line of code:

bgwSource.ReportProgress(1, New String() {"", ""})

is what updates the progress bar by a value of 1 each time it is called. This call is in the outer For statement in the bgwWork function. The problem that you are most likely having is that the call to the WebClient is what is causing the delay you are seeing. What you can do is to set the progress bar to marquee style at the beginning of the bgwWork event so that the progress bar is always incrementing and then leave the rest of the code as I posted it which will reset to count the number of elements in the array. This should solve the issue.

Fernando
0
 
sirbountyAuthor Commented:
Where should I hide my progress bar?
It overlays the combo box, but if I unremark those two lines below, I don't see it progressing at all.

The marquee idea sounds perfect - but if I make that one change, I get
System.Reflection.TargetInvocationException was unhandled
  Message="Exception has been thrown by the target of an invocation."
  Source="mscorlib"
 (seems to throw it as it's completing)


    Public Sub bgwDone(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
 
        Dim sbl As List(Of StringBuilder) = CType(e.Result, List(Of StringBuilder))
        For Each sb As StringBuilder In sbl
            frmMain.cboSearchResults.Items.Add(sb.ToString)
        Next
 
        'frmMain.pbStatus.Visible = False
        'frmMain.cboSearchResults.Visible = True
 
    End Sub

Open in new window

0
 
openshacCommented:
>>Where should I hide my progress bar?

use the RunWorkerCompleted.
0
 
sirbountyAuthor Commented:
Odd - if I put the 'hide' in the completed sub, then the bar doesn't move at all - just disappears when it's done.
If I dont' hide it, it moves, but of course, remains on the form after I no longer need it...
0
 
sirbountyAuthor Commented:
...even tried it on the form when the cbo's visible is true, to set it to false - same thing...
0
 
Fernando SotoCommented:
Hi sirbounty;

Modified my first post to work with setting the ProgressBar to Marquee first and then to normal mode.

Fernando
Imports System.ComponentModel
Imports System.IO
Imports System.Text
Imports System.Text.RegularExpressions
 
Module BgwModule
 
    Public Sub bgwWork(ByVal sender As Object, ByVal e As DoWorkEventArgs)
 
        Dim bgwSource As BackgroundWorker = CType(sender, BackgroundWorker)
        Dim strFood As String = e.Argument.ToString.Replace(" ", "+")
 
        ' Initialize the ProgressBar on the frmMain to Marquee until the WebClient is done
        Dim args() As String = {"Marquee", String.Empty}
        bgwSource.ReportProgress(0, args)
 
        Dim s As Stream
        Dim sr As StreamReader
        Dim sbl As New List(Of StringBuilder)
 
        Using wc As New Net.WebClient
            s = wc.OpenRead("http://www.thedailyplate.com/nutrition/search.php?q=" & strFood)
        End Using
 
        sr = New StreamReader(s)
        Dim r As String = sr.ReadToEnd()
        Dim regex As RegularExpressions.Regex = Nothing
        Dim result As String = regex.Replace(r, "<[^>]*>", String.Empty)
        result = result.Substring(result.IndexOf("&nbsp;", result.IndexOf("search_query")) + 6).Trim.Replace("&nbsp;", "|")
        result = result.Substring(0, result.IndexOf("num_ads") - 8)
        Dim arrData() As String = result.Split("|"c)
 
        ' Initialize the the progress bar to sync to operation
        args(0) = "Initialize"
        args(1) = arrData.Length.ToString()
        bgwSource.ReportProgress(0, args)
 
        For x As Int16 = 0 To CShort(arrData.Length - 1)
            Dim arrItems() As String = arrData(x).Split(CChar(vbLf))
            Dim sb As New StringBuilder
            For Each item As String In arrItems
                If item.Trim <> "" Then
                    If item.IndexOf("erving") = -1 Then
                        sb.Append(item.Trim & " ")
                    Else
                        item = item.Trim
                        sb.Append(item.Substring(item.IndexOf("Calories:")))
                    End If
                End If
            Next
            sbl.Add(sb)
            ' Update the progress bar
            bgwSource.ReportProgress(1, New String() {"", ""})
        Next
        'cboSearchResults.Items.Add(sb.ToString)
        e.Result = sbl
 
        '--tidy up
        sr.Close()
        s.Close()
 
    End Sub
 
    Public Sub bgwDone(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
 
        Dim sbl As List(Of StringBuilder) = CType(e.Result, List(Of StringBuilder))
        For Each sb As StringBuilder In sbl
            frmMain.cboSearchResults.Items.Add(sb.ToString)
        Next
 
    End Sub
 
    Public Sub bwgProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs)
 
        ' Get the array of strings and if first element is Initialize then Initialize the progress bar
        Dim args() As String = CType(e.UserState, String())
        If args(0) = "Initialize" Then
            ' Initialize for normal operations
            frmMain.ProgressBar1.Style = ProgressBarStyle.Blocks
            frmMain.ProgressBar1.Minimum = 0
            frmMain.ProgressBar1.Maximum = Integer.Parse(args(1))
            Return
        End If
        If args(0) = "Marquee" Then
            ' Initialize for continuous operation until WebClient is done
            frmMain.ProgressBar1.Style = ProgressBarStyle.Marquee
            frmMain.ProgressBar1.MarqueeAnimationSpeed = 50
            Return
        End If
 
        ' Increment the progress bar value
        frmMain.ProgressBar1.Increment(e.ProgressPercentage)
 
    End Sub
 
End Module

Open in new window

0
 
sirbountyAuthor Commented:
For some 'odd' reason, adding these to lines back to the bgwDone actually works.
     frmMain.pbStatus.Visible = False
     frmMain.cboSearchResults.Visible = True

A thousand "thank you"s my friend  - this is absolutely perfecto!!! :^)
0
 
sirbountyAuthor Commented:
Most excellent help - as always!!!
0
 
Fernando SotoCommented:
Not a problem, glad to help as always.  ;=)
0

Featured Post

Keep up with what's happening at Experts Exchange!

Sign up to receive Decoded, a new monthly digest with product updates, feature release info, continuing education opportunities, and more.

  • 15
  • 6
  • 4
  • +1
Tackle projects and never again get stuck behind a technical roadblock.
Join Now