DataAdapter.Fill in BackgroundWorkder causes Cross-Thread exception..

cjard

Well-known member
Joined
Apr 25, 2006
Messages
7,081
Programming Experience
10+
I'm trying to figure out why, if I use a DataAdapter to fill a generic DataTable, a grid bound to the table suddenly throws a "Controls must be accessed on the thread where they were created" error.. The whole idea of doing a database operation on a background worker is to avoid halting the UI..

This DataGridView is set to AutoGenerateColumns = true and the schema of the DataTable is not known in advance, which is where i suspect the problem may be occurring (which kinda implies the control itself is bugged?)

VB.NET:
 	System.Windows.Forms.dll!System.Windows.Forms.Control.Handle.get() + 0xdf bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Control.SendMessage(int msg = 11, int wparam = 0, int lparam = 0) + 0x1e bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.Control.BeginUpdateInternal() + 0x4c bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.DataGridView.RefreshColumns() + 0x31 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.DataGridView.RefreshColumnsAndRows() + 0x37 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.DataGridView.DataGridViewDataConnection.ProcessListChanged(System.ComponentModel.ListChangedEventArgs e) + 0x6d bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.DataGridView.DataGridViewDataConnection.currencyManager_ListChanged(object sender, System.ComponentModel.ListChangedEventArgs e = {System.ComponentModel.ListChangedEventArgs}) + 0x27 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.CurrencyManager.OnListChanged(System.ComponentModel.ListChangedEventArgs e) + 0x17 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.CurrencyManager.List_ListChanged(object sender, System.ComponentModel.ListChangedEventArgs e) + 0x139 bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.BindingSource.OnListChanged(System.ComponentModel.ListChangedEventArgs e) + 0x7b bytes	
 	System.Windows.Forms.dll!System.Windows.Forms.BindingSource.InnerList_ListChanged(object sender, System.ComponentModel.ListChangedEventArgs e) + 0x2e bytes	
 	System.Data.dll!System.Data.DataView.OnListChanged(System.ComponentModel.ListChangedEventArgs e = {System.ComponentModel.ListChangedEventArgs}) + 0xf1 bytes	
 	System.Data.dll!System.Data.DataView.ColumnCollectionChanged(object sender, System.ComponentModel.CollectionChangeEventArgs e) + 0x12f bytes	
 	System.Data.dll!System.Data.DataViewListener.ColumnCollectionChanged(object sender, System.ComponentModel.CollectionChangeEventArgs e) + 0x57 bytes	
 	System.Data.dll!System.Data.DataColumnCollection.OnCollectionChanged(System.ComponentModel.CollectionChangeEventArgs ccevent) + 0x5d bytes	
 	System.Data.dll!System.Data.DataColumnCollection.AddAt(int index, System.Data.DataColumn column) + 0xdc bytes	
 	System.Data.dll!System.Data.ProviderBase.SchemaMapping.SetupSchemaWithoutKeyInfo(System.Data.MissingMappingAction mappingAction = Passthrough, System.Data.MissingSchemaAction schemaAction = Add, bool gettingData = true, System.Data.DataColumn parentChapterColumn = null, object chapterValue = null) + 0x15e bytes	
 	System.Data.dll!System.Data.ProviderBase.SchemaMapping.SchemaMapping(System.Data.Common.DataAdapter adapter, System.Data.DataSet dataset, System.Data.DataTable datatable, System.Data.ProviderBase.DataReaderContainer dataReader, bool keyInfo, System.Data.SchemaType schemaType, string sourceTableName, bool gettingData, System.Data.DataColumn parentChapterColumn, object parentChapterValue) + 0x289 bytes	
 	System.Data.dll!System.Data.Common.DataAdapter.FillMapping(System.Data.DataSet dataset, System.Data.DataTable datatable, string srcTable, System.Data.ProviderBase.DataReaderContainer dataReader, int schemaCount, System.Data.DataColumn parentChapterColumn, object parentChapterValue) + 0xbb bytes	
 	System.Data.dll!System.Data.Common.DataAdapter.FillFromReader(System.Data.DataSet dataset = null, System.Data.DataTable datatable = {}, string srcTable = null, System.Data.ProviderBase.DataReaderContainer dataReader = {System.Data.ProviderBase.DataReaderContainer.CommonLanguageSubsetDataReader}, int startRecord = 0, int maxRecords = 0, System.Data.DataColumn parentChapterColumn = null, object parentChapterValue = null) + 0x4b bytes	
 	System.Data.dll!System.Data.Common.DataAdapter.Fill(System.Data.DataTable[] dataTables = {Dimensions:[1]}, System.Data.IDataReader dataReader = {System.Data.OracleClient.OracleDataReader}, int startRecord = 0, int maxRecords = 0) + 0x11c bytes	
 	System.Data.dll!System.Data.Common.DbDataAdapter.FillInternal(System.Data.DataSet dataset, System.Data.DataTable[] datatables, int startRecord, int maxRecords, string srcTable, System.Data.IDbCommand command = {System.Data.OracleClient.OracleCommand}, System.Data.CommandBehavior behavior) + 0xde bytes	
 	System.Data.dll!System.Data.Common.DbDataAdapter.Fill(System.Data.DataTable[] dataTables, int startRecord, int maxRecords, System.Data.IDbCommand command, System.Data.CommandBehavior behavior) + 0xa3 bytes	
 	System.Data.dll!System.Data.Common.DbDataAdapter.Fill(System.Data.DataTable dataTable) + 0x6c bytes	
>	Credaro.exe!Credaro.CredaroMainForm.bgwDBQ_DoWork(object sender = {System.ComponentModel.BackgroundWorker}, System.ComponentModel.DoWorkEventArgs e = {System.ComponentModel.DoWorkEventArgs}) Line 79 + 0x19 bytes
 
You need to create a Delegate on your form that will handle the other thread's events. Any time you try to update one thread's form's UI from a seperate thread, you need a delegate, as if anything goes awry, your app could potentially hang.

Unfortunately I am at home and do not have any examples but someone else does I'm sure. If you search Google for the exception message you received, you will find many examples on how to handle this.
 
The problem is not filling the DataTable. It's binding that table to the grid. When you populate the DataTable it is raising an event in the BindingSource which is then accessing members of the grid. Try calling the SuspendBinding method of the BindingSource first, then call ResumeBinding in the RunWorkerCompleted event handler, when you're back on the UI thread. The BindingSource will then refresh the data-bindings on the correct thread.

I'm not 100% sure that that will work as I've never done it myself. If it doesn't then you'd have to unbind the DataTable form the BindingSource or the BindingSource from the grid first, then rebind afterwards, back on the UI thread.
 
You need to create a Delegate on your form that will handle the other thread's events. Any time you try to update one thread's form's UI from a seperate thread, you need a delegate, as if anything goes awry, your app could potentially hang.
I know this, I'm afraid. When using a background worker, you dont actually need to do the delegation yourself, because all calls to update UI components are done through the ProgressChanged event, which is already delegated. The issue here is that the background worker is updating a non-UI component (a datatable) and then the same thread is, somewhere in microsoft's code, trying to update a Control - take a look at the call stack and you'll see that the exception is occuring in (not my) code; so I'm after proposals of what to do about it.

Unfortunately I am at home and do not have any examples but someone else does I'm sure. If you search Google for the exception message you received, you will find many examples on how to handle this.
Um..
 
The problem is not filling the DataTable. It's binding that table to the grid. When you populate the DataTable it is raising an event in the BindingSource which is then accessing members of the grid.
Mmmh.. And that's the code I think should be delegated (Microsoft's code)

Try calling the SuspendBinding method of the BindingSource first, then call ResumeBinding in the RunWorkerCompleted event handler, when you're back on the UI thread. The BindingSource will then refresh the data-bindings on the correct thread.
I did indeed try that, but it ignored the SuspendBinding call (or seemed to) and still raised events when the schema of the bound datatable changed


I'm not 100% sure that that will work as I've never done it myself. If it doesn't then you'd have to unbind the DataTable form the BindingSource or the BindingSource from the grid first, then rebind afterwards, back on the UI thread.
THis was indeed the approach I had to take..

The code looks something like:

VB.NET:
myBS.DataSource = null
bgw.RunWorkerAsync()

Sub DoWork()
  FillDataTable(dt)
  bgw.ReportProgress(100, null)
End Sub

Sub ProgressChanged()
  myBS.DataSource = dt

Anyone agree I should report this as a bug? I'm sure the grid should query the bindsource on the UI thread, and I'm thinking that it doesnt..
 
It's not a control's responsibility to make itself thread safe. This is a complex situation but it's your responsibility to make sure your use of controls is thread safe.

I just had a closer look at the SuspendBinding method and it's designed to stop changes in the UI being pushed to the data source, not the other way around. I'm not sure but the BeginLoadData and EndLoadData methods of the DataTable may do the job as I think that they will stop the DataTable raising events. You may have to call the ResetBindings method of the BindingSource when you're done.
 
It's not a control's responsibility to make itself thread safe. This is a complex situation but it's your responsibility to make sure your use of controls is thread safe.
OK, but I dont use the control, hence the basis of my confusion. I note also that I dont encounter this error elsewhere and I do a lot of data access?

I just had a closer look at the SuspendBinding method and it's designed to stop changes in the UI being pushed to the data source, not the other way around. I'm not sure but the BeginLoadData and EndLoadData methods of the DataTable may do the job as I think that they will stop the DataTable raising events. You may have to call the ResetBindings method of the BindingSource when you're done.

I shall take a look! Thanks :)
 
Well, you are using the control. Not directly, obviously, but you are accessing data that is bound to the control, so obviously what you do with that data affects that control. The point of data-binding is what affects one affects the other so you can't be surprised when it happens. The stack trace from your exception shows exactly what path execution takes from your filling the table to the grid getting its handle.
 
Sadly, the BeginLoadData route didnt work either..

Well, you are using the control. Not directly, obviously, but you are accessing data that is bound to the control, so obviously what you do with that data affects that control.
Yes, but in MVC, the data and the control's view of it are isolated.

The point of data-binding is what affects one affects the other so you can't be surprised when it happens.
But you could take that argument to the Nth degree of association where everything that happens on the machine affects something else..

The stack trace from your exception shows exactly what path execution takes from your filling the table to the grid getting its handle.
Indeed, and I cant see a way to break into it to ensure that the UI based items behave correctly.. I ought to be able to fill a container on a background thread and not cause problems for the UI. To fix this issue, I'd have to subclass something and override the default handling of events..

End of the day, I still feel this is a bug because it's occurring in a closed shop written and owned by Microsoft - i'm not using the control directly and as a result, I cant make the invokation myself like I would if I *was* using it directly

Further, it doesnt happen all the time. If this datatable were typed, and its schema was NOT changing then I wouldnt have this error (I routinely call myTableAdapter.Fill(myDatatable) with many controls bound to the table through a bindingsource, and there is never a crossthread exception raised) - I dont know why, because filling a table changes it just as does altering its schema..
 
I use a DataAdapter to fill a generic DataTable, a grid bound to the table suddenly throws a "Controls must be accessed on the thread where they were created" error.. The whole idea of doing a database operation on a background worker is to avoid halting the UI..

This DataGridView is set to AutoGenerateColumns = true
I don't get cross-thread exception when doing the same.

You can try setting RaiseListChangedEvents=False on the BindingSource before the Fill and True afterwards, then ResetBindings in worker.completed.

Doesn't BeginLoadData/EndLoadData only work when LoadDataRow is used to load data into the DataTable?
 
I don't get cross-thread exception when doing the same.
I can post some example code, if you like.. it might be in C# though :/

You can try setting RaiseListChangedEvents=False on the BindingSource before the Fill and True afterwards, then ResetBindings in worker.completed.
I shall give it a go

Doesn't BeginLoadData/EndLoadData only work when LoadDataRow is used to load data into the DataTable?
I have no idea.. It's not well documented and reading the decompiled code is horrendous..
 
I don't get cross-thread exception when doing the same.

PS; i never used to. I got a call one day recently that an app i wrote a year ago (and hasnt been updated since) was sudddenly hanging. I traced it to this issue, but as with all things computer that work and then suddenly dont with seemingly nothing changed - i dont like it!
 
Back
Top