在BackgroundWorker中调用ShowDialog函数

7

我有一个WinForms应用程序,其中我的后台工作器正在执行同步任务,添加新文件,删除旧文件等。

在我的后台工作器代码中,我想向用户展示一个自定义窗体,告诉他如果继续,将删除什么和添加什么,并提供YES/NO按钮以获取他的反馈。

我想知道在后台工作器的doWork方法中是否可以这样做? 如果不行,我该怎么办?

请指教。

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
   MyForm f = new MyForm();
   f.FilesToAddDelete(..);
   DialogResult result = f.ShowDialog();
   if(No...)
   return;
   else
   //keep working...
}
4个回答

9
如果您尝试这样做,您会发现它不起作用,因为BackgroundWorker线程不是STA(它来自托管线程池)。
问题的实质在于,您不能从工作线程¹中显示用户界面,因此您必须解决这个问题。您应该传递一个对应用程序的UI元素的引用(主窗体是一个不错的选择),然后使用Invoke将用户交互请求调度到UI线程。下面是一个简单的示例:
class MainForm
{

    // all other members here

    public bool AskForConfirmation()
    {
        var confirmationForm = new ConfirmationForm();
        return confirmationForm.ShowDialog() == DialogResult.Yes;
    }
}

而后台工作人员会执行以下操作:

// I assume that mainForm has been passed somehow to BackgroundWorker
var result = (bool)mainForm.Invoke(mainForm.AskForConfirmation);
if (result) { ... }

¹ 从技术上讲,您无法从不是STA的线程中显示用户界面。如果您自己创建工作线程,则可以选择使其成为STA,但如果它来自线程池,则没有这种可能性。


谢谢Jon,我会尝试一下,看看它对我是否有效。你的回答很有道理。 - Ahmed
顺便说一下,有人告诉我在BackgroundWorker上调用MessageBox.show(..)是安全的,也许是因为该方法是静态的..但它是否也不安全()? - Ahmed
@Ahmed:那个人不知道他们在说什么。这里有一个相关的问题,参见https://dev59.com/g3RB5IYBdhLWcg3wpotm。结论是:你*可以*使用它,但不是因为它是“静态”的或其他类似垃圾的原因。而是因为它专门设置了自己的消息泵。 - Jon
谢谢Jon,您是真正的大师 :) 请查看我参考的有关MessageBox的帖子,在那里被推断它是静态的,没有指向消息泵。https://dev59.com/NmPVa4cB1Zd3GeqP1wd0 - Ahmed
@Ahmed:嗯,那个回答确实没有用最准确的技术术语来表述。我猜想这可能是因为回答的作者和你之间存在着不完美的沟通(毫无疑问,作者知道它为什么有效)。 - Jon
感谢Jon的帮助 :) - Ahmed

4

我通常会创建一个方法在UI线程上执行委托:

  private void DoOnUIThread(MethodInvoker d) {
     if (this.InvokeRequired) { this.Invoke(d); } else { d(); }
  }

通过这个,你可以将你的代码更改为以下内容:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
   DialogResult result = DialogResult.No;
   DoOnUIThread(delegate() {
      MyForm f = new MyForm();
      f.FilesToAddDelete(..);
      result = f.ShowDialog();
   });

   if(No...)
   return;
   else
   //keep working...
}

2

IMO的答案错误,它建议你启动一个线程来处理此问题。你需要做的是将窗口跳转回主调度线程。

在WPF中

public ShellViewModel(
    [NotNull] IWindowManager windows, 
    [NotNull] IWindsorContainer container)
{
    if (windows == null) throw new ArgumentNullException("windows");
    if (container == null) throw new ArgumentNullException("container");
    _windows = windows;
    _container = container;
    UIDispatcher = Dispatcher.CurrentDispatcher; // not for WinForms
}

public Dispatcher UIDispatcher { get; private set; }

然后,当另一个线程(在这种情况下是线程池线程)触发了某个事件:

public void Consume(ImageFound message)
{
    var model = _container.Resolve<ChoiceViewModel>();
    model.ForImage(message);
    UIDispatcher.BeginInvoke(new Action(() => _windows.ShowWindow(model)));
}

WinForms等效功能

不要将UIDispatcher设置为任何值,这样您就可以进行以下操作:

public void Consume(ImageFound message)
{
    var model = _container.Resolve<ChoiceViewModel>();
    model.ForImage(message);
    this.Invoke( () => _windows.ShowWindow(model) );
}

优化WPF代码的DRY原则:

哇,这么多代码……

public interface ThreadedViewModel
    : IConsumer
{
    /// <summary>
    /// Gets the UI-thread dispatcher
    /// </summary>
    Dispatcher UIDispatcher { get; }
}

public static class ThreadedViewModelEx
{
    public static void BeginInvoke([NotNull] this ThreadedViewModel viewModel, [NotNull] Action action)
    {
        if (viewModel == null) throw new ArgumentNullException("viewModel");
        if (action == null) throw new ArgumentNullException("action");
        if (viewModel.UIDispatcher.CheckAccess()) action();
        else viewModel.UIDispatcher.BeginInvoke(action);
    }
}

在视图模型中:

    public void Consume(ImageFound message)
    {
        var model = _container.Resolve<ChoiceViewModel>();
        model.ForImage(message);
        this.BeginInvoke(() => _windows.ShowWindow(model));
    }

希望这有所帮助。

另一个线程应该启动的答案在哪里? - Jon
我从未见过这样的回答作为对我的问题的响应...也许有人创建并删除了它。 - Ahmed
整个互联网上都有这个问题。今天我自己也遇到了这个问题。 :) - Henrik

1

在运行后台工作程序之前,您应该先弹出对话框。在进度更改事件中,您可以更新对话框。


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