views:

757

answers:

6

So I am writing a small Twitter client for me to use. I am using a combination of one big panel, with smaller panels representing the individual tweets. In each smaller panel, I have a PictureBox and a RichTextBox.

Now, my problem is that loading more than 10 tweets causes a slowdown because I am dynamically generating the panels. So I decided to do this using a BackgroundWorker and then add those panels to the main panel.

I've done this numerous times with writing text to a textbox from a different thead(even wrote tutorials on it). Yet I cannot get this to work. I get the error message:

Cross-thread operation not valid: Control '' accessed from a thread other than the thread it was created on.

Code:

List<Panel> panelList = new List<Panel>();

foreach (UserStatus friendStatus in list)
{
    PictureBox pbTweet = new PictureBox();
    // ...
    // code to set numerous properties
    // ...

    RichTextBox rtbTweet = new RichTextBox();
    // ...
    // code to set numerous properties
    // ...

   Panel panelTweet = new Panel();
    // ...
    // code to set numerous properties
    // ...

   panelTweet.Controls.Add(pbTweet);
   panelTweet.Controls.Add(rtbTweet);

   panelList.Add(panelTweet);
}

if (panelMain.InvokeRequired)
    panelMain.BeginInvoke((MethodInvoker)delegate { foreach (Panel p in panelList) { panelMain.Controls.Add(p); } });

Anybody notice any problems?

+4  A: 

panelTweet is created on the BackgroundWorker's thread and is accessed from the main thread in your delegate (panelMain.Controls.Add(p);// p = panelTweet).

You have to call all that function in your main thread, not only the last part.


you can rewrite the function like this:

private void AddControls()
{
    if(panelMain.InvokeRequired)
    {
        panelMain.BeginInvoke(new MethodInvoker(AddControls));
        return;
    }

    foreach (UserStatus friendStatus in list)
    {
        PictureBox pbTweet = new PictureBox();
        // ...
        // code to set numerous properties
        // ...

        RichTextBox rtbTweet = new RichTextBox();
        // ...
        // code to set numerous properties
        // ...

        Panel panelTweet = new Panel();
        // ...
        // code to set numerous properties
        // ...

        panelTweet.Controls.Add(pbTweet);
        panelTweet.Controls.Add(rtbTweet);

        panelMain.Controls.Add(panelTweet)
    }
}
najmeddine
Ahh. I feel like an idiot. Ok, so since one control is created with the UI thread, and the other controls are created in the Backgroundworker thread, how to I do the add? It would seem that I am stuck.
Eclipsed4utoo
it's better to create all controls in the UI thread. I added to my ansewer an example of how to do it all in the UI thread.
najmeddine
A: 

You try to add Panel p created on external thread X back to the winform Thread Y.

Put the whole creation in the BeginInvoke handler. That way all the controls are created in the winform thread Y.

Martijn Laarman
Doesn't that defeat the purpose of using the BackgroundWorker? Won't that still cause the UI to become unresponsive as it's adding the controls?
Eclipsed4utoo
My answer is identical to najmeddine. I take it the creation of the controls don't slow the loading down but rather getting the data.
Martijn Laarman
+1  A: 

When using a background thread, you have to completely separate out the parts that retrieve data from the parts that modify the form controls. All code that modifies the form controls must be invoked on the UI thread, even if it will take some time to do. There is no way round this.

This is normally a good strategy is because, usually, getting the data into memory is the slow part and updating the UI is the fast part (relative to each other).

In your code example, all of the code is the UI-modification part, so it must all go in the UI thread.

EDIT: To optimise the UI part, you could experiment with calling SuspendLayout and ResumeLayout on the panels that you are modifying.

Christian Hayter
Thanks. I started playing around with the SuspendLayout and ResumeLayout, and it seems to be working close to what I want.
Eclipsed4utoo
A: 

Ok, so looking at the answers, it looks like I am just SOL. I need to do all the processing in the UI thread, thereby causing it to become unresponsive.

Eclipsed4utoo
As elder_george said, you should create a lightweight control. Win32 controls (and thus their WinForms wrappers) are actually heave resources. You could try creating your own control that, when OnPaint is called, you draw the user's image (graphics.DrawImage(...)) and their tweets (TextRenderer.DrawText(...)). That would be more lightweight. Alternately, you could use WPF, which are not wrappers around the Win32 controls and are theoretically more lightweight.
Judah Himango
I've already read that UserControls were always "heavier" than the standard controls.
Eclipsed4utoo
+1  A: 

You may try to create controls in ProgressChanged handlers. This way you'll be able to do some initialization (user picture retrieval etc) in background thread, and visualization in GUI thread.

Please note though, that most probably your performance problem is due to big resources required for creating RichTextEdit and PictureBox. Think of creating custom control that will contain only image of user and text, rendered on Paint event, e.g.

elder_george
+1  A: 

You can't create any WinForms UI controls in a background thread.

There are a couple of ways around this - I'd start with:

Control getPanelForUser( UserStatus friendStatus ) {
    PictureBox pbTweet = new PictureBox { /* set props */ };
    RichTextBox rtbTweet = new RichTextBox { /* set props */ };

    Panel panelTweet = new Panel { /* set props */ };

    panelTweet.Controls.Add(pbTweet);
    panelTweet.Controls.Add(rtbTweet);

    return panelTweet;
}

Then in your background worker:

foreach (UserStatus friendStatus in list)
    panelMain.BeginInvoke(
        delegate ( object o ) { 
            panelMain.Controls.Add(getPanelForUser( o as UserStatus ));
        }, 
        friendStatus );

That might still be slow though - it could be worth loading a subset and then drip feeding further ones in. You could also only load the visible list - hide further ones until they scroll. Then you're only loading a page at a time.

Keith