从BackgroundWorker线程访问UI控件

9
我在 Windows 表单上有一个按钮,调用 RunWorkerAsync() 方法,该方法执行一个操作,然后更新同一表单上的 ListBox。
在 DoWork 事件完成后,我为事件分配 Result(即列表),我处理 RunWorkerCompleted() 事件,然后执行以下代码来更新我的 Listbox: listBoxServers.Items.AddRange((List)e.Result);
这时候运行应用程序并按下刷新按钮,会出现以下异常:
System.InvalidOperationException:“从不是创建控件 listBoxServers 的线程访问它。”
如何解决这个问题呢?
编辑:在 DoWork 方法中出现了以下语句而导致的异常: listBoxServers.Items.Clear();
要保持列表最新,需要清除它的内容。

这是WPF还是Windows Forms?您在哪一行代码中遇到了这个异常? - decyclone
@decyclone,我已经更新了我的问题并提供了更多信息。 - Jamie Keeling
6个回答

13

你不能在列表框上调用Invoke方法,而是应该在窗体上调用。对于WinForms应用程序,我会使用类似于以下代码:

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

根据 .NET 版本的不同,您可能需要自己声明一个 MethodInvoker 委托,例如:

public delegate void MethodInvoker();

然而,你也可以考虑使用 Background WorkerReportProgress 特性。相应的事件处理程序应该在表单线程的上下文中被调用。


11

以下是一个我觉得非常方便的代码片段:

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

您可以传递任何 Action 类型的委托或类似以下 lambda 表达式的语句:

ThreadSafe(() =>
{
    [your code here]
});
或者
ThreadSafe(listBoxServers.Items.Clear);

1
这不是WPF吗?WinForms支持Dispatcher吗? - Greg D
“Dispatcher”是WPF类,但这段代码也适用于WinForms。 - Nobody
由于某些原因,IntelliSense无法识别Dispatcher,我已经包含了System.Threading语句。 - Jamie Keeling
3
确保在您的项目中引用了 WindowsBase.dll 。该命名空间为 System.Windows.Threading。http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.aspx - Nobody
Winforms已经有处理这个问题的机制了。没有理由添加对另一个DLL的引用,然后再创建“Dispatcher”对象的开销。只需使用Control.Invoke()Control.BeginInvoke()方法即可,正如正确的答案所建议的那样 - Peter Duniho

9
每当您需要在不同线程间运行某些内容时,您可以尝试以下类似方法:
listBoxServers.BeginInvoke(
    (Action)
    (() => listBoxServers.Items.Clear()));

典型的 Lisp 问题,似乎少了一个括号。 - Hans Passant
谢谢Hans,我已经添加了缺失的括号。 - Chris Conway

1

在Windows应用程序中,不允许后台线程更新UI,因此您必须将控制权还给UI线程进行实际更新。

创建一个方法,在主线程上调用UpdateServerDetails,如下所示:

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

然后调用DispatchServerDetails而不是UpdateServerDetails

一些注意事项:
-这在WPF应用程序中最有效,在WinForms中,您需要跳过一些障碍,或者您可以使用 InvokeRequired
-UI更新仍然是同步的,因此如果UpdateServerDetails执行大量工作,它将阻止UI线程(不是您的情况,只是为了安全起见)。


@Jamie http://stackoverflow.com/questions/975087/wpf-dispatcher-and-running-it-in-background - jan

1
在Windows窗体项目中使用Invoke可能有些棘手,有一些已记录但容易被忽略的陷阱。我建议使用类似于这个问题中所提到的东西:
“是否适合扩展Control以提供一致安全的Invoke/BeginInvoke功能?”
它处理了不需要调用Invoke、从不同线程调用、句柄已经创建或未创建等情况。如果您不喜欢布尔参数,它可以很容易地修改为SafeInvoke()和SafeBeginInvoke()。
(以下是方便您参考的内容:
/// 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();
    }
}

0
我刚刚想到了一种更简单的方法,不需要使用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);

然后在 backgroundworker1_ProgressChanged(object sender, ProgressChangedEventArgs e) 中:

if (e.ProgressPercentage < 0)
{
    //access your ui control safely here
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接