December 1, 2007

Form.Show() From Another THread - C#.Net

Today I ran into a mojor headache. I have a main form that instantiates a secondary form from its constructor. I also register to a receive data event that I have based on a socket. Because the socket uses threading, when my event delegate is called, it is being called from the context of another thread. It is inside the execution of the delegate that I ran into trouble. When I receive data from the socket, I want to open the secondary form and do something with it. Immediately I know that I am going to have to use Invoke on the secondary form in order to safely modify it from the other thread. My original attempt looked like this (I will use an inline delegate here to simplify this listing):

void dataReceived()
{
If(secondaryForm.InvokeRequired)
{
MethodInvoker myDelegate =
delegate
{
secondaryForm.Show();

//do other work on secondaryForm
};

secondaryForm.Invoke(myDelegate);
}
else
{
socondaryForm.Show();
//do other work on secondaryForm
}
}


If I run this code and call myDelegate manually (not from the other thread's event), everything would work fine. The form would show up and do exactly what I wanted. If I run this code and call Show on the secondaryForm manually, close it, then allow the event to call myDelegate, everything would work fine. The problem comes in whenever I run my code and allow the event to call myDelegate before ever showing the secondaryForm.

Whenever execution would get to "secondaryForm.Show()", my form would show up but would be frozen and messed up looking as if it were stuck an infinate loop or something. There would also be no errors thrown. After rearranging my code and retrying different approaches for a while, a light went off. Now I do not claim to understand all of the inner-workings of a windows window and message loop, but I have seen enough details to lead me to the correct solution. With that in mind, the following explaination may be based on some incorrect assumptions. Feel free to correct me if you know better :-)

Since I was calling Invoke from my secondaryForm, the delegate was being called from the context of the secondaryForm. Because of this, all of the modifications that I made to the form thereafter were done safely. Because the problem showed up only when I had not manually opened the form at least once before the event, I knew that it must have something to do with the secondaryForm not being inserted into the message loop properly when it was being called for the first time; or so it appears. The solution:

void dataReceived()
{
If(mainForm.InvokeRequired)
{
MethodInvoker myDelegate =
delegate
{
secondaryForm.Show();

//do other work on secondaryForm
};

mainForm.Invoke(myDelegate);
}
else
{
socondaryForm.Show();
//do other work on secondaryForm
}
}


All that I had to change was to call the Invoke method of the mainForm instead of the secondaryForm, and therefore the InvokeRequired method of the mainForm as well. This is where things get fuzzy for me. For some reason, I had to call the Show method of the secondaryForm from the context of the mainForm in order for the secondaryForm to be entered into the message loop properly. Perhaps this is because mainForm is the primary form (started from Application.Run) that is running on the GUI thread. I am not too sure. For all I know, this may just be a bug in .Net and have nothing to do with message loops and invoke contexts. I hope that someone will at least be able to benefit from this solution :-D If you can shed any light on what exactly is happening here, I welcome your comments.

2 comments:

Sam said...

This whole Invoke, BeginInvoke, etc. stuff is rather confusing. I am not sure I understand completely your program's problem but the secondary form would be owned by the main form if it was declared as a member on the main form. In that case, I think you would need to Invoke the main form.

I wonder if it is an ownership issue--the secondary form is owned by the main form so you would need to Invoke the main form. I could be wrong, as I don't know all the details either of .NET's implementation or your program.

I like your post and look forward to more in the future. Have fun!

KeeperOfTheSoul said...

This particular problem is documented by InvokeRequired at http://msdn2.microsoft.com/en-us/library/system.windows.forms.control.invokerequired.aspx

In particular:
"This means that InvokeRequired can return false if Invoke is not required (the call occurs on the same thread), or if the control was created on a different thread but the control's handle has not yet been created."

As the form has not had its handle created yet and does not have a parent control, InvokeRequired returns false.

Once you call Show however the control will require you to use Invoke, and this is your problem.

Note that trying to access properties/methods can cause the handle to be created on the wrong thread. So you must call Show from the correct thread, by making use of main form.

Personally I prefer to have the background thread raise an event which the main form can listen for, then the main form can worry about invoking, etc.