Invoke does not change thread!?

  • Thread starter Thread starter Carl
  • Start date Start date
C

Carl

Hi,

My complete scenario is a bit to complicated to explain, but my problem is
this:

I start a child-thread from the main thread, the child is not a
BackgroundWorker, but a Thread, since it needs to be in ApartmentState.STA
because it needs to create a Form and display some stuff. It all works
pretty well, unless an exception occurs in the child thread. The application
shows the standard error dialog to the user, and I don't want that. If an
exception occurs, I want to get (at least) the exception message over to the
main thread, and display it there. To accomplish this, I catch the excepion
in the child thread, and does an Invoke call with the string message, to the
main thread. The problem is, it does not switch to the main thread, it's
always the child (using the threads-window in VS to see that)! Very strange,
what might I be doing wrong?

regards

Carl
 
Carl said:
My complete scenario is a bit to complicated to explain, but my problem is
this:

I start a child-thread from the main thread, the child is not a
BackgroundWorker, but a Thread, since it needs to be in ApartmentState.STA
because it needs to create a Form and display some stuff. It all works
pretty well, unless an exception occurs in the child thread. The application
shows the standard error dialog to the user, and I don't want that. If an
exception occurs, I want to get (at least) the exception message over to the
main thread, and display it there. To accomplish this, I catch the excepion
in the child thread, and does an Invoke call with the string message, to the
main thread. The problem is, it does not switch to the main thread, it's
always the child (using the threads-window in VS to see that)! Very strange,
what might I be doing wrong?

At a complete guess, you're calling Invoke on the delegate rather than
on the control. However, a complete example would be better. Could you
post one?

See http://www.pobox.com/~skeet/csharp/complete.html for details of
what I mean by that.
 
Jon Skeet said:
At a complete guess, you're calling Invoke on the delegate rather than
on the control. However, a complete example would be better. Could you
post one?

See http://www.pobox.com/~skeet/csharp/complete.html for details of
what I mean by that.

Hi again,

I'm unable to create such short example, but I've tried to copy/paste the
parts of where the threading problem occurs. There are only three classes
involved, I've added comments to describe what I've tried to do.

////////////////////////////////////////////////////////////////////////
// Code in the main thread
// This code start the entire processing of the reports. This is a Form
class instance.
// The exception message should end up here, so it can be shown.
////////////////////////////////////////////////////////////////////////

private void btnPrint_Click(object sender, EventArgs e)
{
ReportsGateway.CreateReport(userLaen, arnIDs, ardIDs, true, true, this);
}

// I've tried having this callback method in the child thread, but did not
work.
// I would actually rather have it in the child thread, because others might
start
// the report processing in the future.
public void ShowThreadError(string text)
{
throw new BofException("Error generating reports." + Environment.NewLine
+ Environment.NewLine + text);
}


////////////////////////////////////////////////////////////////////////
// Code in the report helper (gateway), an in-between layer.
// This is where the child thread is created. This is a static class,
// inheriting from Object
////////////////////////////////////////////////////////////////////////

public static void CreateReport(int userLaen, int[] arnIDs, int[] ardIDs,
bool onlyPreview, bool printCase, MyBaseForm callingForm)
{
CreateMultiReport(userLaen, arnIDs, ardIDs, callingForm);
}


private static void CreateMultiReport(int userLaen, int[] arnIDs, int[]
ardIDs, MyBaseForm callingForm)
{
object[] agOb = new object[4];

agOb[0] = userLaen;
agOb[1] = arnIDs;
agOb[2] = ardIDs;
agOb[3] = callingForm;

Thread t = new Thread(ReportGenerationForm.StartProcessingReports);
t.SetApartmentState(ApartmentState.STA);
t.IsBackground = true;

ReportGenerationForm.paramObjects = agOb;

t.Start();
}



////////////////////////////////////////////////////////////////////////
// Code in the child thread
// An exception might occur here, that should be pushed up to the main
// UI thread, for nomal display. This is a Form class instance.
////////////////////////////////////////////////////////////////////////

// I've tried to declare this in the main form gui, but no difference.
public delegate void ShowErrorMessage(string text);

public static void StartProcessingReports()
{
ReportGenerationForm frm = new
ReportGenerationForm((int)paramObjects[0],
paramObjects[1] as int[], paramObjects[2] as int[], paramObjects[3]
as MyBaseForm, true, true, true);
IntPtr handle = frm.Handle;
frm.ShowPrint();
}

private void ShowPrint()
{
try
{
ShowDialog();
TopMost = true;
BringToFront();
}
catch (Exception ex)
{
// I've tried lots of different things here, for example calling
Invoke on
// the delegate, calling invoke on the calling form etc etc, but no
use.
ShowErrorMessage d = new ShowErrorMessage((_callingForm as
XXX.YYY.ReportMainthreadForm).ShowThreadError);
IntPtr handle = this.Handle;
this.Invoke(d, new string[] { ex.Message });
}
}

regards

Carl
 
Carl said:
I'm unable to create such short example

Why? What happened when you tried?
but I've tried to copy/paste the
parts of where the threading problem occurs. There are only three classes
involved, I've added comments to describe what I've tried to do.

Well, one thing I notice is that you're calling ShowDialog(),
TopMost=true, and BringToFront() from within the worker thread.

You shouldn't show a UI from a different thread than the one which
created it. That could well be part of the problem. The usual idea with
worker threads is that *non*-UI work is done in the worker thread, and
all UI updates etc are passed by to the UI thread.
 
Carl said:
I'm aware of that GUI should not be messed with from child threads, but this
is old legacy code, and it is far too much to refactor, so I have to find a
workaround.

Well you shouldn't find it surprising that threads aren't behaving
quite as you'd like if you already *know* you're performing invalid
operations on them.
It all works pretty well, except that I can't get the error
messages back to the main thread. I tried again to create a short example,
and I think I got something good enough, which will demonstrate the problem
(using the thread debug window in VS). I'll attach it to this post.

It consists of three classes, one to start things (the main thread gui
class), a helper for the report generation, the acual report generator. I've
added an exception to the report generator.

Okay, there are two forms and you've effectively got two UI threads
running. Your ThreadRunnerForm only ever "sees" the second UI thread,
so when you call Invoke on it it's staying on that second UI thread. If
you want to get back to the UI thread running ThreadStarterForm, you'll
need to call Invoke on the relevant ThreadStarterForm (or some other
control in the same UI thread). If you make your code run
_callingForm.Invoke instead of this.Invoke, it will call back on the
right thread - but that doesn't mean everything will be working as you
really want it to.

I would still recommend trying to avoid using two different UI threads
though. It may well be painful to fix, but I suspect it'll be worth the
effort. Debugging threading issues caused by inappropriate use of UI
threads can be nightmarish.
 
Well you shouldn't find it surprising that threads aren't behaving
quite as you'd like if you already *know* you're performing invalid
operations on them.

Okay, there are two forms and you've effectively got two UI threads
running. Your ThreadRunnerForm only ever "sees" the second UI thread,
so when you call Invoke on it it's staying on that second UI thread. If
you want to get back to the UI thread running ThreadStarterForm, you'll
need to call Invoke on the relevant ThreadStarterForm (or some other
control in the same UI thread). If you make your code run
_callingForm.Invoke instead of this.Invoke, it will call back on the
right thread - but that doesn't mean everything will be working as you
really want it to.

I would still recommend trying to avoid using two different UI threads
though. It may well be painful to fix, but I suspect it'll be worth the
effort. Debugging threading issues caused by inappropriate use of UI
threads can be nightmarish.

Well, I acually thought that it was OK to do it like this, if I did not mess
with the GUI between the threads directly, which I'm not.

The solution you described worked, thanks! I thought that it was 100% OK
thread-wise now, but perhapse it's not?

I think that maybe I should rewrite it even if it's a lot of work, to get it
right, but there is a questions I really must know in that case: When does
it count as a (forbidden) GUI operation? Is it OK to create a Form instance,
which is never showed? Is that GUI-code or not? The quesion might seem
strange, the reason is that I have a ReportViewer on a Form, so I might be
able to create the form to get the report, and send that to the printer,
without showing it.

Thanks again

Carl
 
Carl said:
Well, I acually thought that it was OK to do it like this, if I did not mess
with the GUI between the threads directly, which I'm not.

The solution you described worked, thanks! I thought that it was 100% OK
thread-wise now, but perhapse it's not?

Well, you're never explicitly setting up the message loop on the second
form - and the way the test app works, the message loop won't get much
of a chance to update the progress bar etc, because it'll be busy doing
reporting things. You should really try to keep all your UI operations
on one thread, and your non-UI operations on a different thread.
Currently you've got two UI threads, one of which rarely gets a chance
to actually do UI things.
I think that maybe I should rewrite it even if it's a lot of work, to get it
right, but there is a questions I really must know in that case: When does
it count as a (forbidden) GUI operation? Is it OK to create a Form instance,
which is never showed? Is that GUI-code or not?

That's okay, but a generally bad idea - it makes for confusing code.
The quesion might seem
strange, the reason is that I have a ReportViewer on a Form, so I might be
able to create the form to get the report, and send that to the printer,
without showing it.

I'd certainly extract the reporting aspects from the UI aspects, so you
don't need to create a form at all if you don't want to show one. That
much shouldn't be too hard.
 
Well, I acually thought that it was OK to do it like this, if I did not
Well, you're never explicitly setting up the message loop on the second
form - and the way the test app works, the message loop won't get much
of a chance to update the progress bar etc, because it'll be busy doing
reporting things. You should really try to keep all your UI operations
on one thread, and your non-UI operations on a different thread.
Currently you've got two UI threads, one of which rarely gets a chance
to actually do UI things.


That's okay, but a generally bad idea - it makes for confusing code.


I'd certainly extract the reporting aspects from the UI aspects, so you
don't need to create a form at all if you don't want to show one. That
much shouldn't be too hard.

Thanks a lot for your help, I think I will extract the report stuff, like
you suggest.

regards

Carl
 
Back
Top