简体   繁体   中英

Updating the GUI from background worker

The name of the question is: " Updating the GUI from background worker ", but the correct name world be: " Updating the GUI from background worker OR reporting multiple-variables (other than an integer) from background worker "

Please let me explain my situation. In a program I have a background worker which analyses the information.As the result of this analysis - form GUI elements should be populated with necessary data. In GUI I would like to update

  • 2 datagridviews
  • 1 listbox
  • 5 labels

As I understand - I can only natively report 1 int value via ReportProgress() method of background worker.

So the question is - how can I pass a List<> ( + some other variables: string , int ) via ReportProgress() ? Basically - i want to update the GUI with the information but "1 integer" just won't do.. So either it should be possible to pass multiple variables via an ReportProgress() OR i can use an Invoke from inside the BackgroundWorker itself to update the GUI.. Personally I don't like the Invoke approach... What's your opinion?

Here is my code (see the comments):

   private void button9_Click(object sender, EventArgs e) // start BW
    {
        bw.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
        bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted);
        bw.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);

        bw.WorkerReportsProgress = true;
        bw.WorkerSupportsCancellation = true;

        bw.RunWorkerAsync(10);
    }

    private void button10_Click(object sender, EventArgs e) // cancel BW
    {
        bw.CancelAsync();
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        int count = (int)e.Argument;
        for (int i = 1; i <= count; i++)
        {
            if (bw.CancellationPending)
            {
                e.Cancel = true;
                break;
            }

            List<List<string>> list_result = new List<List<string>>();
            list_result = Proccess();

            bw.ReportProgress(list_result.Count()); // right now I can only return a single INT

            /////////// UPDATE GUI //////////////
            // change datagridview 1 based on "list_result" values
            // change datagridview 2
            // change listbox
            // change label 1
            // change label ..          

            Thread.Sleep(20000);
        }

        MessageBox.Show("Complete!");
        e.Result = sum;
    }

    private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        prog_count++;
        listBox1.Items.Add("Count: (" + prog_count.ToString() + "/20). Found: " + e.ProgressPercentage.ToString() + ".");
    }

There's a UserState parameter when calling ReportProgress .

var list_result = new List<List<string>>();

new backgroundWorker1.ReportProgress(0, list_result);

The parameter type is an object so you'll have to cast it back to the type you need:

void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    var userState = (List<List<string>>)e.UserState;
}   

The tricky issue with this is, how do you determine whether you're passing back a List , or a list of lists, or a single string, number, etc. You'll have to test for each possibility in the ProgressChanged event.

void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    var myList = e.UserState as List<List<string>>;
    if (myList != null)
    {
        // use list
        return;
    }

    int myNumber;
    if (Int32.TryParse(e.UserState.ToString(), out myNumber))
    {
        // use number
        return;
    }

    var myString = e.UserState.ToString();
    // use string
}

Alternatively, you could create a class that holds all the values you need (or use Tuple ), run everything in the background to populate that class, then pass that to the RunWorkerCompleted event, and update your UI all at once from there.

I have written two very easy methods that enable you to invoke your code (only if required) and you only need to write your code once. I think this makes Invoke much friendlier to use:

1) BeginInvoke

public static void SafeBeginInvoke(System.Windows.Forms.Control control, System.Action action)
{
    if (control.InvokeRequired)
        control.BeginInvoke(new System.Windows.Forms.MethodInvoker(() => { action(); }));
    else
        action();
}

2) Invoke

public static void SafeInvoke(System.Windows.Forms.Control control, System.Action action)
{
    if (control.InvokeRequired)
        control.Invoke(new System.Windows.Forms.MethodInvoker(() => { action(); }));
    else
        action();
}

It can be called like this:

SafeInvoke(textbox, () => { textbox.Text = "text got changed"; });

Alternatively you could just

System.Windows.Forms.Form.CheckForIllegalCrossThreadCalls = false;

(which only changes behaviour in debug mode btw) and look if you run into problems.
More often than not you actually don't. It took me quite some time to find cases very Invoke is really required for things not to get messed up.

The basic pattern for updating the UI from another thread is:

If controlItem.InvokeRequired Then
    controlItem.Invoke(Sub() controlItem.Text = textUpdateValue)
Else
    controlItem.Text = textUpdateValue
End If

This could update your list of controls without requiring you to pass anything through ReportProgress. If you would like to update your control from within the thread, I don't believe you need to check InvokeRequired, because it will always be required. However, best practices might be to expose the setting of a control via a property and then to do the full check so you can call it from anywhere.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM