eMi
eMi

Reputation: 5618

Backgroundworker blocks UI

I try to perform an easy task in an other backgroundthread, so the UI doesn't get blocked, but it still gets blocked. Did I forget anything?

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

    private void doWorkButton_Click(object sender, EventArgs e)
    {
        if (backgroundWorker.IsBusy != true)
        {
            // Start the asynchronous operation.
            backgroundWorker.RunWorkerAsync();
        }
    }

    private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        //BackgroundWorker worker = sender as BackgroundWorker;
        if (textBoxOutput.InvokeRequired)
        {
            textBoxOutput.Invoke(new MethodInvoker(delegate
            {
                for (int i = 0; i < 10000; i++)
                {
                    textBoxOutput.AppendText(i + Environment.NewLine);
                }
            }));
        }
    }
}

While the textBox gets filled, the UI is blocked:

enter image description here

Upvotes: 2

Views: 5368

Answers (3)

Rick Davin
Rick Davin

Reputation: 1041

Your app wants to repeatedly send updates from the background thread to the UI. There is a built-in mechanism for this: the ProgressChanged event for the background worker. A ReportProgress call is triggered in the background, but executes on the UI thread.

I do change one thing, however. Performance can degrade with too many cross-thread calls. So instead of sending an update every iteration, I instead will batch them into 100.

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        const int maxIterations = 10000;
        var progressLimit = 100;
        var staging = new List<int>();
        for (int i = 0; i < maxIterations; i++)
        {
            staging.Add(i);
            if (staging.Count % progressLimit == 0)
            {
                // Only send a COPY of the staging list because we 
                // may continue to modify staging inside this loop.
                // There are many ways to do this.  Below is just one way.
                backgroundWorker1.ReportProgress(staging.Count, staging.ToArray());
                staging.Clear();
            }
        }
        // Flush last bit in staging.
        if (staging.Count > 0)
        {
            // We are done with staging here so we can pass it as is.
            backgroundWorker1.ReportProgress(staging.Count, staging);
        }
    }

    // The ProgressChanged event is triggered in the background thread
    // but actually executes in the UI thread.
    private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        if (e.ProgressPercentage == 0) return;
        // We don't care if an array or a list was passed.
        var updatedIndices = e.UserState as IEnumerable<int>;
        var sb = new StringBuilder();
        foreach (var index in updatedIndices)
        {
            sb.Append(index.ToString() + Environment.NewLine);
        }
        textBoxOutput.Text += sb.ToString();
    }

EDIT:

This requires you set the background worker's WorkerReportsProgress property to true.

It's not important that you pass a count with the ReportProgress call. I do so just to have something and to quickly check if I can return.

One really should keep in mind about how many events are being invoked and queued up. Your original app had 10,000 cross thread invocations and 10,000 changed text events for textBoxOutput. My example uses 100 cross thread calls since I use a page size of 100. I could still have generated 10,000 changed text events for the textbox, but instead use a StringBuilder object to hold a full page of changes and then update the textbox once for that page. That way the textbox only has 100 update events.

EDIT 2

Whether or not your app needs paging is not the main deal. The biggest take away should be that the background worker really should use ReportProgress when trying to communicate info back to the UI. See this MSDN Link. Of particular note is this:

You must be careful not to manipulate any user-interface objects in your DoWork event handler. Instead, communicate to the user interface through the ProgressChanged and RunWorkerCompleted events.

Upvotes: 4

John
John

Reputation: 3702

Your invocation code should be outside the loop. Everything in the invoked codeblock, will be executed on the UI thread, thus blocking it.

    private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        //BackgroundWorker worker = sender as BackgroundWorker;
        for (int i = 0; i < 10000; i++)
        {
            // do long-running task

            //if (textBoxOutput.InvokeRequired)
            //{
                textBoxOutput.Invoke(new MethodInvoker(delegate
                {
                    textBoxOutput.AppendText(i + Environment.NewLine);
                }));
            //}
        }
    }

Upvotes: 2

Icepickle
Icepickle

Reputation: 12796

an easier way would be to do completely create your output text, and then paste the full output into the TextBox, then you only need one invocation

protected delegate void SetTextDelegate(TextBox tb, string Text);

protected void SetText(TextBox tb, string Text)
{
    if (tb.InvokeRequired) {
        tb.Invoke(new SetTextDelegate(SetText), tb, Text);
        return;
    }
    tb.Text = Text;
}

and then inside your dowork

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    StringBuilder sb = new StringBuilder();
    //BackgroundWorker worker = sender as BackgroundWorker;
    for (int i = 0; i < 10000; i++)
    {
         sb.AppendLine(i.ToString());
    }
    SetText(textBoxOutput, sb.ToString());
}

Upvotes: 1

Related Questions