views:

307

answers:

5

Hi, i am working with a winforms control that is both a GUI element and also does some internal processing that has not been exposed to the developer. When this component is instantiated it may take between 5 and 15 seconds to become ready so what i want to do is put it on another thread and when its done bring it back to the gui thread and place it on my form. The problem is that this will (and has) cause a cross thread exception.

Normally when i work with worker threads its just with simple data objects i can push back when processing is complete and then use with controls already on the main thread but ive never needed to move an entire control in this fashion.

Does anyone know if this is possible and if so how? If not how does one deal with a problem like this where there is the potential to lock the main gui?

+3  A: 

You don't need to lock the GUI, you just need to call invoke:

Controls in Windows Forms are bound to a specific thread and are not thread safe. Therefore, if you are calling a control's method from a different thread, you must use one of the control's invoke methods to marshal the call to the proper thread. This property can be used to determine if you must call an invoke method, which can be useful if you do not know what thread owns a control. ref

Here is how it looks in code:

public delegate void ComponentReadyDelegate(YourComponent component);
public void LoadComponent(YourComponent component)
{
    if (this.InvokeRequired)
    {
        ComponentReadyDelegate e = new ComponentReadyDelegate(LoadComponent);
        this.BeginInvoke(e, new object[]{component});
    }
    else
    {
        // The component is used by a UI control
        component.DoSomething();
        component.GetSomething();
    }
}

// From the other thread just initialize the component
// and call the LoadComponent method on the GUI.
component.Initialize(); // 5-15 seconds
yourForm.LoadComponent(component);

Normally calling the LoadComponent from another thread will cause a cross-thread exception, but with the above implementation the method will be invoked on the GUI thread.

InvokeRequired tells you if:

the caller must call an invoke method when making method calls to the control because the caller is on a different thread than the one the control was created on. ref

Update:
So if I understand you correctly the control object is created on a thread other than the GUI thread, therefore even if you were able to pass it to the GUI thread you still won't be able to use it without causing a cross-thread exception. The solution would be to create the object on the GUI thread, but initialize it on a separate thread:

public partial class MyForm : Form
{
    public delegate void ComponentReadyDelegate(YourComponent component);
    private YourComponent  _component;
    public MyForm()
    {
        InitializeComponent();
        // The componet is created on the same thread as the GUI
        _component = new YourComponent();

        ThreadPool.QueueUserWorkItem(o =>
        {
            // The initialization takes 5-10 seconds
            // so just initialize the component in separate thread
            _component.Initialize();

            LoadComponent(_component);
        });
    }

    public void LoadComponent(YourComponent component)
    {
        if (this.InvokeRequired)
        {
            ComponentReadyDelegate e = new ComponentReadyDelegate(LoadComponent);
            this.BeginInvoke(e, new object[]{component});
        }
        else
        {
            // The component is used by a UI control
            component.DoSomething();
            component.GetSomething();
        }
    }
}
Lirik
Doesn't this suffer from the same problem that the O.P. wanted to avoid? Namely, he wants to initialise his control in the background so it doesn't block the GUI thread as it takes 15 seconds to initialize. If Invoke is used to marshall execution back to the GUI thread, will that not still block the GUI thread?
Tim Long
@Tim, I assume that the component will already be initialized in a separate thread when the OP calls yourForm.LoadComponent. The GUI will not be burdened with the initialization.
Lirik
hi, im not sure if ive made myself clear. using delegates and background workers isnt working as all they are doing is passing back a reference to my object. The object is still on the worker thread and the gui thread cant make use of it.
Grant
@Grant, create the object on the GUI thread and initialize it on the worker thread (since you said the initialization takes 5-10 seconds). I've updated my answer with that in mind.
Lirik
Thanks Lirik your suggestion seems to work. The only thing I've noticed is that even though its on a separate thread and its initialized via the begin invoke - its still behaving as though its all on the one thread again (blocks the UI for 10+ seconds). Maybe because the object is on the main thread invoking to it blocks it just the same as if there was no worker thread at all. does that make sense?
Grant
@Grant If you initialize the component via BeginInvoke, then it will cause the initialization to occur on the UI thread since it was created on the UI thread... I just realized this problem. Trying to think of a workaround. It's a catchy question.
Lirik
Yeah i know. one of the more challenging multi threading issues i've had to deal with..
Grant
@Grant, can you briefly tell us what does the initialization of the component entail? Can you put any of the initialization parts on a separate thread?
Lirik
Not really sure TBH. Its a closed component. It goes off and does some data fetching and processing from a remote location. So theres TCP activity and number crunching, database etc. Have been over the component using redgate reflector and cant see any public methods or ways to separate it out.
Grant
@Grant, I don't know if I can be of any further help here... let's see if anybody else can come up with a better solution.
Lirik
No worries, Thanks anyway :)
Grant
+1  A: 

Without knowing too much about the object. To avoid cross thread exceptions, you can make the initial thread invoke a call (Even if you are calling from a thread).

Copied and pasted from one of my own applications :

 private delegate void UpdateStatusBoxDel(string status);

    private void UpdateStatusBox(string status)
    {
        listBoxStats.Items.Add(status);
        listBoxStats.SelectedIndex = listBoxStats.Items.Count - 1;
        labelSuccessful.Text = SuccessfulSubmits.ToString();
        labelFailed.Text = FailedSubmits.ToString();
    }

    private void UpdateStatusBoxAsync(string status)
    {
        if(!areWeStopping)
            this.BeginInvoke(new UpdateStatusBoxDel(UpdateStatusBox), status);
    }

So essentially the threaded task will call the "Async" method. Which will then tell the main form to begininvoke (Actually async itself).

I believe there is probably a shorter way to do all of this, without the need for creating delegates and two different methods. But this way is just ingrained into me. And it's what the Microsoft books teach to you do :p

Pyronaut
A: 

The BackgroundWorker class is designed for exactly this situation. It will manage the thread for you, and let you start the thread, as well as cancel the thread. The thread can send events back to the GUI thread for status updates, or completion. The event handlers for these status and completion events are in the main GUI thread, and can update your WinForm controls. And the WinForm doesn't get locked. It's everything you need. (And works equally well in WPF and Silverlight, too.)

Cylon Cat
hi, im not sure if ive made myself clear. using delegates and background workers isnt working as all they are doing is passing back a reference to my object. The object is still on the worker thread and the gui thread cant make use of it.
Grant
The backgroundworker object can send progress messages to the GUI thread, and can include an object in the message. That will make the object available to the GUI thread.
Cylon Cat
A: 

The control must be created and modified from the UI thread, there's no way around that.

In order to keep the UI responsive while doing long-running initialization, keep the process on a background thread and invoke any control access. The UI should remain responsive, but if it doesn't, you can add some wait time to the background thread. This is an example, using .Net 4 parallel tools: http://www.lovethedot.net/2009/01/parallel-programming-in-net-40-and_30.html

If interaction with the specific control being initialized can't be allowed until initialization finishes, then hide or disable it until complete.

Ragoczy
Hi Ragocrazy, Im not following. If the control is created and modified on the UI thread (main thread) - how can i keep the process on a background thread??
Grant
Try to divide your work into processing that doesn't directy affect the UI and the UI updates, then use BeginInvoke (or Invoke) for the UI updates. This way all the processing that can is done on the background thread and you only marshal to the UI thread when necessary. So, for insance, adding items to a listbox -- all the retrieval from a database, formating of text, etc, can be done on the background thread and then only the Add() has to use BeginInvoke. The rest of the UI will still be responsive to the user (and the listbox, too, if you make it visible).
Ragoczy
A: 

I'm a bit late to the party here, but if I understand the problem correctly, it seems like you're trying to use a control from a 3rd party library and said control is trying to do way too much.

Unfortunately, I don't think you'll be able to initialize the control in a separate thread. Your best bet is using slight-of-hand to make the form behave in a manner the user expects while the control loads.

For instance, you could disable the form while everything is being initialized (I sincerely hope for your sake that this control only needs to initialize once), switch to a loading cursor, and display a process bar. A background worker and a timer should be enough to keep the process bar moving at a decent clip.

You might also be able to do something logically similar to double buffering the form. Create a shallow version of the form you're using, allow basic operations while the real version loads, copy the information the user has entered from the shallow version to the deep version and switch them out. Unfortunately, I'm not at a computer with Visual Studio installed, so I'm not sure this is possible without causing the window to flicker.

Finally, look into replacing the control. Something that does that much work in one place is probably poorly designed and will continue to cause problems down the road.

RevolXadda