Banshee
Banshee

Reputation: 15847

Threading with WinForms?

In my application I startup a hidden dummyForm that is merely created to keep track of the main UI thread. So If a new form is about to be created InvokeRequired is used on the dummy form to make sure that we are on the main UI thread when creating the new form.

Directly after instantiating my frmStart form I check the frmStart.InvokeRequired and it is set to false so no need for invoke here (the same goes for dummyForm.InvokeRequired).

Then I got a frmMyDialog that will use frmStart as parent/owner something like this:

using(Create frmMyDialog on main UI thread)
{
    frmMyDialog.Show(frmStart);
}

This will throw a cross thread exception and the strange thing here is that:

frmMyDialog.InvokeRequired = false
dummyForm.InvokeRequired = false
frmStart.InvokeRequired = true

And this is even when I'm checking that dummyForm.InvokeRequired is false when creating the frmStart?

The frmMyDialog.InvokeRequired should always be the same value as dummyForm.InvokeRequired? What is happening here?

I have checked that the frmStart and dummyForm is not re-created at all after the first instance has been created.

Edit1:

This is how the application starts :

public static void Main(string[] args)
{
     _instance = new MyClientMain(parameters);
     Application.Run(_instance);
}

The constructor of MyClientMain class will run Setup on a static class called MainControl. MainControler will in the setup method instanciate a dummyform like this :

if (_dummyForm == null)
   _dummyForm = new Form();

After this is done a login form will handle a login and this form is multithreaded. When the login is finished the MainController will be called again to instanciate and open the main MDI windo that holds frmStart. To make sure that we are on the same thread the following is done :

public static StartApplication()
{
if (_dummyForm.InvokeRequired)
                    _dummyForm.Invoke(new MethodInvoker(delegate { OpenMainOrbitWindow(); }));
     //Instanciate mainform and frmStart then open mainForm with frmStart as a MDI child

}

There is no multithreading here.

Then when the service goes offline a event will be triggered and I need to popup a frmMyDialog but when using .ShowDialog() it dialog will be placed behind forms so the parent/owner most be found and set like this :

public static Form GetActiveForm()
        {
            Form activeForm = Form.ActiveForm;

            if (activeForm != null)
                return activeForm;

            if (MainOrbitForm.TopMost)
                return MainOrbitForm;
            else
            {
                FormCollection openForms = Application.OpenForms;
                for (int i = 0; i < openForms.Count && activeForm == null; ++i)
                {
                    Form openForm = openForms[i];
                    if (openForm.IsMdiContainer)
                        return openForm.ActiveMdiChild;
                }
            }

            if (_patientForm != null)
            {
                if (_patientForm.TopMost)
                    return _patientForm;
            }

            return null;
        }

        public static string ShowOrbitDialogReName()
        {
            frmMyDialog myDialog;
            Form testForm;

            //Makes sures that the frmOrbitDialog is created with the same thread as the dummyForm
            //InvokeRequired is used for this
            using (myDialog = MainController.CreateForm<frmOrbitDialog>())
            {
               //Settings...
               testForm = GetActiveForm();
               myDialog.ShowDialog(GetActiveForm(testForm));


            }
        }

The problem is that

myDialog.InvokeRequired = false
testForm.InvokeRequired = true;
MainController.DummyForm.InvokeRequired = false;

Edit2: Startup and creates the dummyform :

dummyForm.InvokeRequired = false
Thread.CurrentThread.ManagedThreadId = 9 

After success login we create the mainform

_mainForm.InvokeRequired = false
MainControl.DummyForm.InvokeRequired = false
Thread.CurrentThread.ManagedThreadId = 9

Everything looks fine so far. Then a callback is received(WCF) and a event creates a frmMyDialog on the same thread(Invoke is used on the dummyForm) and then the ShowDialog is used :

frmMyCustomDialog.ShowDialog(_mainForm)

This throws a CrossThreadException and this is how it looks like at this point :

_mainForm.InvokeRequired = true
frmMyCustomDialog.InvokeRequired = false
MainControl.DummyForm.InvokeRequired = false
Thread.CurrentThread.ManagedThreadId = 12

Why is the MainControl.DummyForm not true? The ManageThreadId is not 9 but 12?

Upvotes: 4

Views: 664

Answers (3)

Vladimir Perevalov
Vladimir Perevalov

Reputation: 4157

You should use System.Threading.SynchronizationContext.Current. It was created directly for purposes like this. You may access it anywhere, after the first form of your app have been created. Judging by your example below, this should not be a problem, since you create a form right o the start of application.

public static void Main(string[] args)
{
     _instance = new MyClientMain(parameters);
     Application.Run(_instance);
}

Then anywhere, you need the code to be executed on UI thread, you simply use

System.Threading.SynchronizationContext.Current.Send() // To execute your code synchronously
System.Threading.SynchronizationContext.Current.Post() // To execute your code synchronously

Mind, that SynchronizationContext is smartenough to see, that you call it already from UI thread, and then it will simply execute your delegate directly. And also, remember that you need to create some WinForms form or control before you use SynchronizationContext for the first time, becase when you do this, context will be initialized to appropriate implementation.

There are 3 implementation: Default, that does nothing - just always run code in sync, it stays in the Current until you create WinForms control or WPF control. Then Current will be populated with either context for Winforms, or context for WPF dispatcher.

Upvotes: 2

CWilliams
CWilliams

Reputation: 213

This is just off the top of my head, but as Vladimir Perevalov has stated in a different discussion, you have to make your form visible.

If your frmDummy is never shown, then it never has it's window handle created and assigned, and therefore it will always reply False to "InvokeRequired". This would mean that all the code you mean to sync through frmDummy is never actually sent to the initial UI thread, but is always run in the current thread. (which becomes an UI thread of its own for the control it has just created).

The important thing to note is that InvokeRequired tries to determine if the said control's window handle is owned by another thread. It has nothing to do with the constructor.

If you do not want to show the frmDummy, you can call CreateControl right after you instantiate it, to make sure it has it's handle assigned.

Upvotes: 1

Alex
Alex

Reputation: 8116

I didn't fully understand your question, because your examples don't really show anything about multithreading - however, if you want to create a form where the parent is another form from another thread, you could use this code:

public void CreateShowDialogForm()
{
  if (this.InvokeRequired)
  {
    this.Invoke(new Action(CreateShowDialogForm));
  }
  else
  {
    Form frmMyDialog = new Form();
    frmMyDialog.Show(this);
  }
}

private void Form4_Load(object sender, EventArgs e)
{
  Task t = new Task(() => CreateShowDialogForm());
  t.Start();
  t.ContinueWith(task => true);
}

Upvotes: 0

Related Questions