Accessing UI Control from BackgroundWorker Thread

2019-01-03 17:29发布

I have a button on my windows form that calls the RunWorkerAsync() method, this in turn performs an action which then updates a ListBox on the same form.

After the DoWork event has finished I assign the Result for the event (Which is a list), I process the RunWorkerCompleted() event and then perform the following code to update my Listbox

alt text

which calls this:

alt text

(Apologies, code formatting won't work)

Now when I run the application and press the refresh button the following exception appears:

alt text

How would I get around this?

Edit:

The exception is thrown on the folowing statement, this occurs in the DoWork method where I clear the contents to keep the list up to date;

listBoxServers.Items.Clear();

6条回答
我欲成王,谁敢阻挡
2楼-- · 2019-01-03 17:53

Here's a snippet which I find very handy:

public static void ThreadSafe(Action action)
{
    Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Normal, 
        new MethodInvoker(action));
}

You can pass it any delegate of Action type or simply a lambda like this:

ThreadSafe(() =>
{
    [your code here]
});

or

ThreadSafe(listBoxServers.Items.Clear);
查看更多
贪生不怕死
3楼-- · 2019-01-03 17:59

What I've done is something like this every time you need to run something across threads:

listBoxServers.BeginInvoke(
    (Action)
    (() => listBoxServers.Items.Clear()));
查看更多
啃猪蹄的小仙女
4楼-- · 2019-01-03 18:00

Background threads are not allowed to update the UI in Windows applications, so you have to revert the control back to the UI thread for the actual update.

Create a method that will call UpdateServerDetails on the main thread, like this:

private void DispatchServerDetails(List<ServerDetails> details)
{
  Action<List<ServerDetails>> action = UpdateServerDetails;
  Dispatcher.Invoke(action)
}

and then call DispatchServerDetails instead of UpdateServerDetails.

Some caveats:
-This works best in WPF applications, for WinForms, you'll need to jump through some hoops, or you can use InvokeRequired
-The UI update is still synchronous, so if UpdateServerDetails does a lot of work, it will block the UI thread (not your case, just to be on the safe side).

查看更多
放荡不羁爱自由
5楼-- · 2019-01-03 18:08

I just figured out a simpler way without using Invoke:

int fakepercentage = -1;
//some loop here......if no loop exists, just change the value to something else
if (fakepercentage == -1)
{
    fakepercentage = -2;
}
else
{
    fakepercentage = -1;
}
backgroundworker1.ReportProgress(fakepercentage);

Then in backgroundworker1_ProgressChanged(object sender, ProgressChangedEventArgs e):

if (e.ProgressPercentage < 0)
{
    //access your ui control safely here
}
查看更多
虎瘦雄心在
6楼-- · 2019-01-03 18:12

You may not call Invoke on the list box, but on the form. For WinForms applications I use something like:

...
this.Invoke((MethodInvoker)delegate()
{
    // Do stuff on ANY control on the form.
});
...

Depending on the .NET version, you may have to declare a delegate for MethodInvoker yourself as

public delegate void MethodInvoker();

However, you might also consider using the ReportProgress feature of the Background Worker. The respective event handler should be called in the context of the form's thread.

查看更多
欢心
7楼-- · 2019-01-03 18:15

Using Invoke in a windows forms project can be a little tricky, there're some pitfalls that are documented but easy to miss. I recommend using something like you'll find in this question:

Is it appropriate to extend Control to provide consistently safe Invoke/BeginInvoke functionality?

It handles cases where invoke is not required, is called from different threads, handle is or isn't created, etcetcetc. It could be easily modified to be SafeInvoke() and SafeBeginInvoke() if you're not a fan of the bool parameter.

(Included here for your convenience:

/// Usage:
this.lblTimeDisplay.SafeInvoke(() => this.lblTimeDisplay.Text = this.task.Duration.ToString(), false);

// or 
string taskName = string.Empty;
this.txtTaskName.SafeInvoke(() => taskName = this.txtTaskName.Text, true);


/// <summary>
/// Execute a method on the control's owning thread.
/// </summary>
/// <param name="uiElement">The control that is being updated.</param>
/// <param name="updater">The method that updates uiElement.</param>
/// <param name="forceSynchronous">True to force synchronous execution of 
/// updater.  False to allow asynchronous execution if the call is marshalled
/// from a non-GUI thread.  If the method is called on the GUI thread,
/// execution is always synchronous.</param>
public static void SafeInvoke(this Control uiElement, Action updater, bool forceSynchronous)
{
    if (uiElement == null)
    {
        throw new ArgumentNullException("uiElement");
    }

    if (uiElement.InvokeRequired)
    {
        if (forceSynchronous)
        {
            uiElement.Invoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); });
        }
        else
        {
            uiElement.BeginInvoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); });
        }
    }
    else
    {
        if (!uiElement.IsHandleCreated)
        {
            // Do nothing if the handle isn't created already.  The user's responsible
            // for ensuring that the handle they give us exists.
            return;
        }

        if (uiElement.IsDisposed)
        {
            throw new ObjectDisposedException("Control is already disposed.");
        }

        updater();
    }
}
查看更多
登录 后发表回答