[RESOLVED] 2005 - Problem with Backgroundworker and UI
I have an grid for displaying email retrieved from a pop email account. The headers are retreived one at a time using a backgroundworker. It supplies a datarow to the main thread with the new email data. The datarow is added to the grid datasource which is a datatable. From the user's perspective, one new row at a time is added to the grid until all emails are retrieved.
Except for users with very fast pop servers. For them, the grid is not refreshing quickly enough to keep up with the background worker supplying new rows. They end up seeing rows of the same data duplicated over and over and skipped rows. I tried using a thread monitor and it's not working. The only thing that worked was setting a 1 second join on the backgroundworker, and this isn't a solution since it slows everyone down, even the already slow connections, instead of solving the issue. Why can't a grid keep up with the data being supplied. Is there a better way to provide the data to the grid?
It's easy to test with a simple application. Add a datagrid, a backgroundworker and a button to a form. Add this code to the form:
Code:
Private ds As New DataTable
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
BackgroundWorker1.WorkerReportsProgress = True
ds.Columns.Add("Row Number", GetType(Integer))
DataGridView1.DataSource = ds
Dim cls As New Class1
BackgroundWorker1.RunWorkerAsync(cls)
End Sub
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim worker As System.ComponentModel.BackgroundWorker
worker = CType(sender, System.ComponentModel.BackgroundWorker)
Dim cls As Class1 = CType(e.Argument, Class1)
cls.GetData(worker, e)
End Sub
Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
Dim state As Class1.CurrentState = _
CType(e.UserState, Class1.CurrentState)
ds.ImportRow(state.dr)
End Sub
Then also add a class to the project:
Code:
Public Class Class1
Public Class CurrentState
Public dr As DataRow
End Class
Public Sub GetData(ByVal worker As System.ComponentModel.BackgroundWorker, _
ByVal e As System.ComponentModel.DoWorkEventArgs)
Dim dr As DataRow
Dim state As New CurrentState
Dim table0 As New DataTable
table0.Columns.Add("Row Number", GetType(Integer))
dr = table0.Rows.Add
For x As Integer = 1 To 50
dr.Item("Row Number") = x
state.dr = dr
worker.ReportProgress(0, state)
System.Threading.Thread.CurrentThread.Join(10)
Next
End Sub
End Class
Play with the CurrentThread.Join timeout when you run the project. Notice that if you set it to a very low number, the grid does not give you a sequential row numbering. It skips and repeates row numbers. That simulates a fast pop server turning data to the grid very quickly. Set a high timout, and there is no problem. That simulates a very slow pop server and the UI is able to keep up with the data being supplied to it. If I add a SyncLock or Monitor.TryEnter, it never stops thread supplying data and the grid still does not update correctly. I get the exact same results.
If I arbitrarily decide to have my application set a high timeout, my users with fast pop servers are going to start screaming because their fast email is now crawling. Is there a better solution?
Re: 2005 - Problem with Backgroundworker and UI
The problem is that you are only using one DataRow object. If your background thread runs quickly then the Row Number column of that DataRow will have been updated before the main thread gets a chance to get it. I just ran your code as is and the first 4 rows all contained to number 4. That means that your worker thread was able to execute four iterations of the loop before the main thread was able to read the data.
The answer is simple. I changed your second code snippet to this:
Code:
Public Class Class1
Public Class CurrentState
Public dr As DataRow
End Class
Public Sub GetData(ByVal worker As System.ComponentModel.BackgroundWorker, _
ByVal e As System.ComponentModel.DoWorkEventArgs)
Dim dr As DataRow
Dim state As CurrentState
Dim table0 As New DataTable
table0.Columns.Add("Row Number", GetType(Integer))
dr = table0.Rows.Add
For x As Integer = 1 To 50
state = New CurrentState
dr.Item("Row Number") = x
state.dr = dr
worker.ReportProgress(0, state)
System.Threading.Thread.CurrentThread.Join(10)
Next
End Sub
End Class
and it behaved exactly as you want. The difference is that each iteration is creating a new DataRow, so any that went previously are unaffected, even if the main thread takes a while to get to them.
Re: 2005 - Problem with Backgroundworker and UI
jmcilhinney, I was counting on you answering! Thanks so much. I had to further ammend the code a bit to get the results, but you were spot on with the reason it wasn't working. I've been banging my head on this one, so thanks a million. Here's what worked with no join at all - putting the whole table creation into the loop:
Code:
Public Sub GetData(ByVal worker As System.ComponentModel.BackgroundWorker, _
ByVal e As System.ComponentModel.DoWorkEventArgs)
Dim state As CurrentState
For x As Integer = 1 To 50
Dim dr As DataRow
Dim table0 As New DataTable
table0.Columns.Add("Row Number", GetType(Integer))
dr = table0.Rows.Add
state = New CurrentState
dr.Item("Row Number") = x
state.dr = dr
worker.ReportProgress(0, state)
Next
End Sub
Re: [RESOLVED] 2005 - Problem with Backgroundworker and UI
Woah! You've gone too far the other way now. You should NOT be creating a DataTable inside the loop. You don't need 50 different DataTables; only 50 different DataRows. You only need 1 DataTable so it should be created outside the loop. That 1 DataTable can create all 50 DataRows for you.
Also, what's the point of adding the 50 DataRows to the DataTable anyway? You never use that DataTable anyway.
Re: [RESOLVED] 2005 - Problem with Backgroundworker and UI
When I added only the state to the loop, I was still seeing the wrong results. So I guess I went too far, you're right! I definitely don't need 50 tables, but if I don't create new rows in the loop, it doesn't work for me.
Re: [RESOLVED] 2005 - Problem with Backgroundworker and UI
Ah, I see what you're saying. I didn't look closely enough at the code. You're quite correct that you must create new DataRows inside the loop. I said that but my code didn't actually do it. You certainly only need one DataTable to create all the new rows though.
Re: [RESOLVED] 2005 - Problem with Backgroundworker and UI
Issue resolved. Excellent results. Thanks so much!