views:

368

answers:

5

I have a windows form that gets data from a scale via the serial port. Since I need the operator to be able to cancel the process I do the get data process on a second thread.

The data collection process gets multiple readings from the scale one at a time. The form has a label that needs to be updated with information specific to each reading.

I call the method to get the data from the scale with this code.

Dim ad As New readALine(AddressOf thisScale.readALine)
Dim ac As New AsyncCallback(AddressOf Me.GetDataCallback)
Dim iar As IAsyncResult = ad.BeginInvoke("", ac, ad)

The delegate for the readALine method is defined in the UI code.

Delegate Function readALine(ByVal toSend As String) As String

The GetDataCallback method:

Private Sub GetDataCallback(ByVal ia As IAsyncResult)
    .
    .
    .
    lblInstructions.Text = _thisMeasure.dt.Rows(_currRow - 1).Item("cnt") & ":"
    lblInstructions.Refresh()
    .
    .
    .
End Sub

I get the exception on the "lblInstructions.Text =" statement.

I thought the GetDataCallback method was part of the UI thread so don't understand why I'm getting the exception. I know this could probably be rewritten using a BackgroundWorker and it's appropriate events but for now would like to understand why this isn't working as expected.

The application was written originally in VS2003 and just recently upgraded to VS2008.

Any insight will be appreciated.

Thanks, Dave

+1  A: 

user-interface controls can only be updated by the thread that created them

try

yourForm.BeginInvoke

instead; that should marshall the call to the correct thread

Steven A. Lowe
A: 

In .NET 1.1 it was possible to perform these cross-thread operations, even though they weren't safe.

In .NET 2.0 the exception you mention is thrown, when you try to perform cross-thread operations such as this, which means that you are performing the UI operations on a non-UI thread, even though you thought you weren't.

Try using the Me.InvokeRequired method to determine whether you are currently on the UI thread. E.g. you could define a method to perform the necessary tasks:

Protected Sub PerformUIOperations()
  If Me.InvokeRequired Then
    'We are currently on a non-UI thread. Invoke this method on the UI thread:
    Me.Invoke(New MethodInvoker(AddressOf Me.PerformUIOperations))
    Return
  End If
  'Perform UI operations when we know it is safe:
  lblInstructions.Text = "blabla"
End Sub

The PerformUIOperations method can then be called from any non-UI thread, since it takes care of performing the tasks on the correct thread itself.

Of course, if you need to pass parameters to the PerformUIOperations method (such as information regarding the ongoing operation) you'll have to define a delegate to match the PerformUIOperations method signature and use this instead of the MethodInvoker.

Bernhof
+1  A: 

The problem is a confusion over BeginInvoke. Calling Control.BeginInvoke marshals a delegate call to the UI thread. Calling BeginInvoke on a delegate causes it to be executed on a thread pool thread - and any callback you provide is executed on the same (thread pool) thread. The latter is what you're doing, which is why GetDataCallback is being executed on a thread pool thread, not the UI thread..

So, within GetDataCallback you need to call Control.Invoke or Control.BeginInvoke to marshal back to the UI thread.

One point to note: I very rarely use Control.InvokeRequired these days - it's simpler to unconditionally call Invoke/BeginInvoke; the performance difference isn't usually enough to make up for the benefit in readability, in my experience.

Jon Skeet
Jon,I have the following in another section of the code:Delegate Sub setValueCallback(ByVal row As Integer, ByVal value As Decimal)Public Sub setValue(ByVal row As Integer, ByVal value As Decimal) If Me.Controls.Item(_tbIndex(row - 1)).InvokeRequired Then Dim d As New setValueCallback(AddressOf setValue) Me.Invoke(d, New Object() {row, value}) Else Dim tb As TextBox = Me.Controls.Item(_tbIndex(row - 1)) tb.Text = value _dt.Rows(tb.Tag).Item(1) = value End IfEnd SubHow would this be rewritten to not use .InvokeRequired?
Dave Spicer
That didn't work very well, I'll post it as an answer instead.
Dave Spicer
Can my original code be rewritten in a way that marshals the delegate call to the UI thread?
Dave Spicer
Not easily, I'm afraid.
Jon Skeet
A: 

Jon,

I have the following in another section of the code:

Delegate Sub setValueCallback(ByVal row As Integer, ByVal value As Decimal)
Public Sub setValue(ByVal row As Integer, ByVal value As Decimal)
    If Me.Controls.Item(_tbIndex(row - 1)).InvokeRequired Then
        Dim d As New setValueCallback(AddressOf setValue)
        Me.Invoke(d, New Object() {row, value})
    Else
        Dim tb As TextBox = Me.Controls.Item(_tbIndex(row - 1))
        tb.Text = value
        _dt.Rows(tb.Tag).Item(1) = value
    End If
End Sub

How would this be rewritten to not use .InvokeRequired?

Dave Spicer
In C# I'd do it with an anonymous function. (An anonymous method or a lambda expression.) In VB10 that may be the way forward too. In VB9 you could have a separate method (e.g. `MarshallingSetValue`) which would just call `Control.Invoke` with a delegate instance from `SetValue`. Then `SetValue` would unconditionally set the value.
Jon Skeet
A: 

Dave, maybe this is the solution you are looking for:

Dim ad As New readALine(AddressOf thisScale.readALine)
Dim ac As New AsyncCallback(AddressOf Me.GetDataCallback)
Dim iar As IAsyncResult = ad.BeginInvoke("", ac, ad)

Delegate Function readALine(ByVal toSend As String) As String

Private Sub GetDataCallback(ByVal ia As IAsyncResult)
   If lblInstructions.InvokeRequired Then
      lblInstructions.Invoke(New AsyncCallback(AddressOf GetDataCallback), New Object() {ia})
   Else
     .
     .
     lblInstructions.Text = _thisMeasure.dt.Rows(_currRow - 1).Item("cnt") & ":"
     lblInstructions.Refresh()
     .
     .
  End If
End Sub
Charles Y.