cross-thread UI updating?

  • Thread starter Thread starter spacemarine
  • Start date Start date
S

spacemarine

hello,

i am a long time asp.net developer, but new to winform programming.

in the application ive been assigned, the mainform spawns a new thread
to fetch a bunch of data from a method. this takes time, which is why
its been placed into its own thread, allowing the rest of the ui to be
usable:

Private m_getDashboardDataThread As Thread
...
'this is the method that gets the data and *currently* binds it to UI
m_getDashboardDataThread = New Thread(AddressOf GetDashboardData)

'not sure if this check is needed
If Not m_getDashboardDataThread.ThreadState =
Threading.ThreadState.Running Then
m_getDashboardDataThread.IsBackground = True
m_getDashboardDataThread.Start()
End If

problem: how & where do i properly bind this data? currently its doing
the databind in the thread-method. but thats not going to work when i
move the DA method to the real DAL. plus, ive heard allowing a spawned
thread access to the main form UI is a Bad Thing (tm). this true, btw?

anyway... what is the proper way to do this? the app needs to spawn a
thread to get the data, get it back somehow and update the UI.


thanks for your help,
sm
 
(e-mail address removed) schreef:
anyway... what is the proper way to do this? the app needs to spawn a
thread to get the data, get it back somehow and update the UI.

Here is a pattern i very often see for thread-safe updates of the UI...
(Method defined in a class that derives from Form)

public void UpdateSomeData(Data data)
{
if (this.InvokeRequired)
{
// call this method again, but on the ui thread...
this.EndInvoke(this.BeginInvoke(new MethodInvoker(delegate()
{
this.UpdateSomeData(data);
} )));
}
else
{
// we are in the ui thread, do the real work
this.dataGridView.DataSource = data;
}

}
 
Here is a pattern i very often see for thread-safe updates of the UI...
(Method defined in a class that derives from Form)

thanks, tim. but can you help me put this in context? for instance,
heres some pseudo code i have now:

SomeForm.vb
--------------

'bad, because GetMyData() is binding its data (from a thread) back to
the UI:
Private Sub SomeForm_Load(ByVal sender As Object, ByVal e As
System.EventArgs) Handles MyBase.Load

Dim getDataThread As Thread = New Thread(AddressOf GetMyData)

If Not getDataThread.ThreadState = Threading.ThreadState.Running
Then
getDataThread.IsBackground = True
getDataThread.Start()
End If

End Sub

....but im a bit in the dark as to how the .Invoke pattern works w/
this, how it ties back into my code. can you shed any light on this?


thanks!
sm
 
// call this method again, but on the ui thread...
this.EndInvoke(this.BeginInvoke(new MethodInvoker(delegate()

Just a point of detail here. this.EndInvoke(this.BeginInvoke()) is
unecessarily complicated.

If you want the worker thread to block until the method invoked in the UI
thread has completed (which is what you do by calling
this.EndInvoke(this.BeingInvoke())), simply call this.Invoke() (where this
represents a Control). No need for BeginInvoke / EndInvoke here. Note that
using Control.Invoke can lead to some very subtle dead-locks if you are
sharing data protected by locks between the worker thread and the UI
thread.

If you want the worker thread to invoke a method in the UI thread but not
wait for its completion before continuing its job, simply call
this.BeginInvoke() (where this represents a Control). There is no need to
call Control.EndInvoke() (this is the only case in the .NET standard Class
Library in which the call to EndInvoke() after a BeginInvoke() is not
required).
 
If you want the worker thread to invoke a method in the UI thread but not
wait for its completion before continuing its job, simply call
this.BeginInvoke() (where this represents a Control). There is no need to
call Control.EndInvoke() (this is the only case in the .NET standard Class
Library in which the call to EndInvoke() after a BeginInvoke() is not
required).

that sounds similar to whati found this informative MSDN article:

http://msdn.microsoft.com/msdnmag/issues/03/02/Multithreading/

....which shed some serious light. using it, heres what i came up with:



Private m_data As MyData

...

Private Sub LoadForm()

'a built-in delegate for calling parameter-less methods (wont work
if FillMyData()
'takes an argument, use different one or write one)

Dim mi As MethodInvoker = New MethodInvoker(AddressOf FillMyData)
mi.BeginInvoke(Nothing, Nothing)

End Sub


Private Sub FillMyData()

'get data
m_data = FillFromWebService()

Dim eventArgs() As Object = {Me, System.EventArgs.Empty}

'update UI's label:

'this is from MSDN article. but wont compile!?
statusLabel.BeginInvoke(New System.EventHandler(UpdateUI),
eventArgs)

'update UI's visual control:

'this is from MSDN article. but wont compile!?
myControl.BeginInvoke(New System.EventHandler(BindUIData),
eventArgs)

End Sub


Private Sub UpdateUI(ByVal sender As Object, ByVal e As
System.EventArgs)
'this method is called via Control.Invoke, so its allowed to touch
UI
statusLabel.Text = "done!!"
End Sub


Private Sub BindUIData(ByVal sender As Object, ByVal e As
System.EventArgs)
'this method is called via Control.Invoke, so its allowed to touch
UI
myControl.BindData(m_data)
End Sub



this sample uses "Asynhronous Delegate Invocation" for its threading.
it uses it once to spawn the worker-thread, and from inside that it
uses it twice more to tap back into the UI-thread (a label update, and
a databind).

but as you can see, theres a problem -- the work-process' attempt to
use Control.BeginInvoke() doesnt compile. says:

"'System.EventHandler' is a delegate type. Delegate construction
permits only a single AddressOf expression as an argument list. Often
an AddressOf expression can be used instead of a delegate
construction."

....i tried using AddressOf, and that failed as well:

"'AddressOf' expression cannot be converted to 'System.Delegate'
because 'System.Delegate' is not a delegate type."


hmm... so close! anyone help a brother out? can you see something im
missing? .NET v1.1.


thanks,
sm
 
'this is from MSDN article. but wont compile!?
statusLabel.BeginInvoke(New System.EventHandler(UpdateUI),
eventArgs)

I'm not very familiar with VB .NET but I think that you'd need to have this
instead:
statusLabel.BeginInvoke(New System.EventHandler(AddressOf UpdateUI),
eventArgs)

(note the AddressOf which is not required - and in fact not legal - in C#
but required AFAIK in VB .NET).

A couple of additional points:

- in the Load event handler of your form, you are calling
MethodInvoker.BeginInvoke() to call the method which is going to retrieve
data from your Web Service in a worker thread instead of calling it
directly from the UI thread:

Dim mi As MethodInvoker = New MethodInvoker(AddressOf FillMyData)
mi.BeginInvoke(Nothing, Nothing)

Accessing a web service in a worker thread instead of the UI thread is a
good idea as web service calls can block for sometimes a very long time
which means that they could freeze the UI if made from the UI thread.

However, when you call MethodInvoker.BeginInvoke(), what happens is that
the method that you passed as a parameter to BeginInvoke() is executed in a
thread from the system ThreadPool. The system ThreadPool only contains a
limited number of thread (25 per processor by default) and is used by the
..NET Framework itself to perform async operations. If all the ThreadPool
threads are already being used to perform some operations, any other
pending operation placed in the ThreadPool by yourself or the .NET
Framework is going to be queued until one of the ThreadPool's thread
becomes free again. It is therefore crucial that any operation performed in
a thread from the ThreadPool takes as little time as possible in order to
avoid starving the ThreadPool.

In your particular case, the method that you want executed in a ThreadPool
thread (FillMyData) calls a Web Service method which can take a very long
time to execute (seconds or even minutes). It's typically the kind of
operation that should not be executed in a thread from the ThreadPool.
Instead, use the asynchronouse methods that are provided by your Web
Service proxy class that has been generated by Visual Studio when you added
a Web Reference in your project (these methods are called BeginXXX - for
example if your web service has a method called GetData, then the proxy
class should contain a method called BeginGetData which allows you to call
GetData asynchronously without having to rely on the system ThreadPool).

- When you are calling Control.BeginInvoke() in your code, you are using
the EventHandler delegate to pass the address of the function you want
executed in the UI thread. This is perfectly fine in your particular case.

However, it is generally not a very good idea to use the EventHandler
delegate when calling Control.Invoke or Control.BeginInvoke as, in this
case, EventHandler will ignore the values that you passed for the 'sender'
and 'e' parameter and always use the control on which Invoke or BeginInvoke
has been called as the sender and always use EventArgs.Empty as the 'e'
parameter.

This is actually documented in the Control.Invoke() documentation but I've
been badly bitten by this weird behaviour a little while ago. I've spent a
long time trying to figure out why the 'sender' parameter was set to some
random value instead of being set to whatever I has set it. I eventually
figured out that EventHandler did its own thing completely ignoring the
parameters that were passed to it so I got rid of it and used custom
delegates which do not modify their parameters behind my back.
 
I'm not very familiar with VB .NET but I think that you'd need to have this
instead:
statusLabel.BeginInvoke(New System.EventHandler(AddressOf UpdateUI),
eventArgs)

(note the AddressOf which is not required - and in fact not legal - in C#
but required AFAIK in VB .NET).

me neither -- im used to C#, but this group uses only VB. you are
correct in the compile error, VB required the AddressOf. i missed this
because the MSDN article was written in C# only, and my conversion
software didnt pick that up.

thanks!
In your particular case, the method that you want executed in a ThreadPool
thread (FillMyData) calls a Web Service method which can take a very long
time to execute (seconds or even minutes). It's typically the kind of
operation that should not be executed in a thread from the ThreadPool.
Instead, use the asynchronouse methods that are provided by your Web
Service proxy class that has been generated by Visual Studio when you added
a Web Reference in your project (these methods are called BeginXXX - for
example if your web service has a method called GetData, then the proxy
class should contain a method called BeginGetData which allows you to call
GetData asynchronously without having to rely on the system ThreadPool).

that is great to know. i will bring this up w/ my team.

figured out that EventHandler did its own thing completely ignoring the
parameters that were passed to it so I got rid of it and used custom
delegates which do not modify their parameters behind my back.

this is very good to know. i realize now my posted code was sloppy and
mismatched.. case in point, even tho i was using Asynchronous Delegate
Invocation to fire the worker-thread, i then switched to the
EventHandler style to kickback to the UI-thread. but as im not writing
a custom event-args class, i think it makes more sense just to do a
repeat ADI...so i switched it to:

Me.BeginInvoke(New MethodInvoker(AddressOf UpdateUI))

....and that works fine.



thanks again!

sm
 
Back
Top