13

UPDATE: Just to summarize what my question has boiled down to:

I was hoping that constructing .NET forms and controls did NOT create any window handles -- hoping that process was delayed until Form.Show/Form.ShowDialog

Can anyone confirm or deny whether that is true?


I've got a large WinForms form with tab control, many many controls on the form, that pauses while loading for a couple seconds. I've narrowed it down to the designer generated code in InitializeComponent, rather than any of my logic in the constructor or OnLoad.

I'm well aware that I can't be trying to interact with the UI on any thread other than the main UI thread, but what I'd like to do is to have the application pre-load this form (run the constructor) in the background, so it's ready for display on the UI thread instantly as soon as the user wants to open it. However, when constructing in a background thread, on this line in the designer:

this.cmbComboBox.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.Suggest;

I'm getting the error

Current thread must be set to single thread apartment (STA) mode before OLE calls can be made. Ensure that your Main function has STAThreadAttribute marked on it.

Now this is halfway down the designer file, which gives me hope that in general this strategy will work. But this particular line seems to be trying to instantly kick off some kind of OLE call.

Any ideas?

EDIT:

I think I'm not making myself clear here. The delay seems to take place during the construction of a bazillion controls during the designer-generated code.

My hope was that all this initialization code took place without actually trying to touch any real Win32 window objects since the form hasn't actually been shown yet.

The fact that I can set (for example) Label texts and positions from this background thread gave me hope that this was true. However it may not be true for all properties.

Andy G
  • 18,518
  • 5
  • 42
  • 63
Clyde
  • 7,723
  • 8
  • 53
  • 83
  • is the exception thrown only for the cmbox or for all controls. Because if so you could set the property as the last thing – Vivek Bernard Jan 15 '10 at 16:27
  • just for setting the AutoCompleteMode on a combo box. Above this in the designer is plenty of code setting Text/Name/Position/Size/etc. properties on controls. – Clyde Jan 15 '10 at 16:29
  • While it's 'preloading' the form, what will your application be doing? Showing a "please wait" message maybe? – Codesleuth Jan 15 '10 at 16:29
  • then try setting that proprty alone as the last thing i'm mean after the thread is complte – Vivek Bernard Jan 15 '10 at 16:34
  • 1
    They do whatever they want while it's preloading -- that's the whole point of putting it in the background thread. – Clyde Jan 15 '10 at 16:41
  • They can do what they want with your application? Seems funny that you give people 2-3 seconds to start another action in your app, then interrupt that action once the form load is complete. – Eric J. Jan 15 '10 at 17:13
  • 1
    @Clyde: If you just want to show a "please wait" message that can handle user input, you can use another thread to show that, then have the main thread launch the form with many controls. – Codesleuth Jan 17 '10 at 10:22

6 Answers6

17

While it is not possible to create a form on one thread, and display it using another thread, it is certainly possible to create a form in a non main GUI thread. The current accepted answer seems to say this is not possible.

Windows Forms enforces the Single Threaded Apartment model. In summary this means that there can only be one Window message loop per thread and vice versa. Also, if for example threadA wants to interact with the message loop of threadB, it must marshal the call through mechanisms such as BeginInvoke.

However, if you create a new thread and provide it with it's own message loop, that thread will happily process events independently until it is told to end the message loop.

So to demonstrate, below is Windows Forms code for creating and displaying a form on a non GUI thread:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        label1.Text = Thread.CurrentThread.ManagedThreadId.ToString();

    }

    private void button1_Click(object sender, EventArgs e)
    {
        ThreadStart ts = new ThreadStart(OpenForm);

        Thread t = new Thread(ts);
        t.IsBackground=false;

        t.Start(); 
    }

    private void OpenForm()
    {
        Form2 f2 = new Form2();

        f2.ShowDialog();
    }
}


public partial class Form2 : Form
{
    public Form2()
    {
        InitializeComponent();
    }

    private void Form2_Load(object sender, EventArgs e)
    {
        label1.Text = Thread.CurrentThread.ManagedThreadId.ToString() ;

    }
}

The OpenForm method runs in a new thread and creates an instance of Form2.

Form2 is actually given it's own separate message loop by calling ShowDialog(). If you were to call Show() instead, no message loop would be provided and Form2 would close immediately.

Also, if you try accessing Form1 within OpenForm() (such as using 'this') you will receive a runtime error as you are trying to do cross-thread UI access.

The t.IsBackground=false sets the thread as a foreground thread. We need a foreground thread because background threads are killed immediately when the main form is closed without first calling FormClosing or FormClosed events.

Apart from these points, Form2 can now be used just like any other form. You'll notice that Form1 is still happily running as usual with it's own message lopp. This means you can click on the button and create multiple instances of Form2, each with their own separate message loop and thread.

You do need to be careful about cross Form access which is now actually cross-thread. You also need to ensure that you handle closing of the main form to ensure any non main thread forms are closed correctly.

Ash
  • 57,515
  • 31
  • 146
  • 168
  • Your understanding of the message loop is formidable but really spinning up a second message loop (how ever you do it) is not generally a good idea because lots of things can get confusing. Like focus, tabbing, keyboard and mouse capture etc. – Jan Bannister Jan 18 '10 at 09:21
  • Jan, yes having a separate message loop is probably more useful if you want to create a separate rendering window for video, animation etc. – Ash Jan 18 '10 at 09:28
4

I think your understanding is a little off. Controls must be touched from the thread that created them, not the main UI thread. You could have numerous UI threads in a application, each with its own set of controls. Thus creating a control on a different thread will not allow you to work with it from the main thread without marshalling all of the calls over using Invoke or BeginInvoke.

EDIT Some references for multiple UI threads:

MSDN on Message Loops MSDN social discussion Multiple threads in WPF

dsolimano
  • 8,272
  • 3
  • 45
  • 61
  • Can you provide a reference for this claim? My understanding is that the same thread processing the message loop needs to update controls. – Eric J. Jan 15 '10 at 16:38
  • Right....the point I'm trying to make is that I'm not intending to update any onscreen element. This is all initialization code, the form hasn't been shown. I'm only trying to get the .NET object construction out of the way early. – Clyde Jan 15 '10 at 16:46
  • Eric - you can have N number of message loops, one per thread, the controls have thread affinity Clyde - The act of creating a .Net object creates the underlying Windows object, which is bound to the thread on which is created it. – dsolimano Jan 15 '10 at 16:56
  • @dsolimano: One of your references states that **winforms** can only have one UI thread. The question is about winforms. Under Windows.Forms, there is only one GUI thread (the one that calls Application.Run). "All controls should be created on that thread. Under WPF, it's possible to have multiple GUI threads, but each one's controls belong only to that one thread." Do you have an example that illustrates creating a second UI thread under WinForms? – Eric J. Jan 15 '10 at 17:10
  • I dont think its possible: http://stackoverflow.com/questions/1566791/run-multiple-ui-threads and http://stackoverflow.com/questions/106378/getting-multiple-ui-threads-in-windows-forms – SwDevMan81 Jan 15 '10 at 18:21
  • @SwDevMan81 - http://stackoverflow.com/questions/1566791/run-multiple-ui-threads says that it is possible, and I've also been doing it for a while with no problems. I can't release the code as it's my company's. @Eric J - if you read further down in that conversation, the user who made the initial statement is corrected. WinForms objects are basically just underlying windows objects, and multiple UI threads are possible in raw Windows. Do you have any MSDN references where it says that it's not possible? – dsolimano Jan 15 '10 at 18:45
  • @dsolimano - I'm not trying to argue that it's not possible, just understand for sure that it is and how to do it. Everything I have read on the topic of threading and WinForms presents the approach of all forms on the main UI thread and all background processing on background threads. That's not to say it's the only way, just that it's the way that's commonly discussed and the only one I know. – Eric J. Jan 15 '10 at 20:13
  • Found a decent discussion on the advanced-dotnet mailing list: http://www.mail-archive.com/advanced-dotnet@discuss.develop.com/msg11274.html – dsolimano Jan 15 '10 at 22:00
  • 2
    What you can't do in Windows Forms is have one Form containing 2 controls with a separate thread handling events for each control. I think the confusion is that dsolimano is (correctly) including a Form as being a Control. Taking this meaning, each thread can have it's own Form (Control), but all Buttons etc placed on the Form must belong to the same thread. – Ash Jan 16 '10 at 04:43
3

The answer is no.

If you create a window handle on any thread other than the GUI thread you can never show it.

Edit: It is completely possible to create Forms and controls and display them in a thread other than the main GUI thread. Of course if you do this you can only access the multi threaded GUI from the thread that created it, but it is possible. – Ashley Henderson

You need to perform any heavy lifting on a bg thread and then load the data into you GUI widget

Jeremy Thompson
  • 52,213
  • 20
  • 153
  • 256
Jan Bannister
  • 4,592
  • 8
  • 35
  • 44
  • Can you confirm for me that the constructor of a .NET control does in fact create the window handle? Is there any documentation on that? – Clyde Jan 15 '10 at 17:41
  • Never mind ... I'm seeing for myself that the Handle is populated after construction. – Clyde Jan 15 '10 at 18:58
  • 4
    @Jan, this is incorrect. It is completely possible to create Forms and controls and display them in a thread other than the main GUI thread. Of course if you do this you can only access the multi threaded GUI from the thread that created it, but it is possible. – Ash Jan 16 '10 at 03:49
  • @Ash my point was that 'no, you can never show it' not that you could not new up the object, which is possible but academic and not in the spirit of what Clyde was asking about. – Jan Bannister Jan 18 '10 at 09:15
  • 3
    Ash is right. I just switched my application to use one thread per form and it is able to show them all. "The UI thread" really means "the thread on which you created the UI entity of interest." – Fantius Oct 06 '10 at 02:49
1

In general, properties of the form need to be accessed from the same thread running the message loop. That means, in order to construct the form on another thread, you would need to marshal any calls to actually set properties using BeginInvoke. This is true for property sets from the constructor, too, if they end up generating a message that needs to be processed (as is happening to you now).

Even if you get that to work, what does it buy you? It will be a bit slower, not faster, overall.

Perhaps just show a splash screen while this form is loading?

Alternatively, review why your form takes so long to construct in the first place. It's not common for this to take seconds.

Eric J.
  • 139,555
  • 58
  • 313
  • 529
  • This makes the most sense to me. Check out the MethodInvoker delegate for samples on how to do this. The bg thread is doing the leg work, while the GUI thread is still running independent and accepting user input. – spoulson Jan 15 '10 at 18:59
1

I believe it is possible to add the components created on the non-UI thread to the main UI, I've done it.

So there are 2 threads, 'NewCompThread' and 'MainThread'.

You spin off NewCompThread and it creates components for you - all ready to be displayed on the MainUI (created on MainThread).

But ... you WILL get an exception if you try something like this on NewCompThread: ComponentCreatedOnNewCompTHread.parent = ComponentCreatedOnMainThread;

But you can add this:

if (ComponentCreatedOnMainThread.InvokeRequired) {
  ComponentCreatedOnMainThread.Invoke(appropriate delegate...);
} else {
  ComponentCreatedOnNewCompTHread.parent = ComponentCreatedOnMainThread; 
}

And it will work. I've done it.
The strange thing (to me) is that then the ComponentCreatedOnNewCompTHread 'thinks' it was created on the MainThread.

If you do the following from the NewCompThread: ComponentCreatedOnNewCompTHread.InvokeRequired it will return TRUE, and you'll need to create a delegate and use Invoke to get back to the MainThread.

KateToo
  • 31
  • 4
0

Creating a control in a background thread is possible but only on an STA thread.

I created an extension method in order to use this with the async/await pattern

private async void treeview1_AfterSelect(object sender, TreeViewEventArgs e)
{
    var control = await CreateControlAsync(e.Node);
    if (e.Node.Equals(treeview1.SelectedNode)
    {
        panel1.Controls.Clear();
        panel1.Controls.Add(control);
    }
    else
    {
        control.Dispose();
    }
}

private async Control CreateControlAsync(TreeNode node)
{
    return await Task.Factory.StartNew(() => CreateControl(node), ApartmentState.STA);
}

private Control CreateControl(TreeNode node)
{
    // return some control which takes some time to create
}

This is the extension method. Task does not allow to set the apartment so I use a thread internally.

public static Task<T> StartNew<T>(this TaskFactory t, Func<T> func, ApartmentState state)
{
    var tcs = new TaskCompletionSource<T>();
    var thread = new Thread(() =>
    {
        try
        {
            tcs.SetResult(func());
        }
        catch (Exception e)
        {
            tcs.SetException(e);
        }
    });
    thread.IsBackground = true;
    thread.SetApartmentState(state);
    thread.Start();
    return tcs.Task;
}
Jürgen Steinblock
  • 26,572
  • 21
  • 100
  • 169