我试图实现的是在每次操作后向RichTextBox添加文本。问题是,这些操作需要一些时间,而不是在每个操作完成后查看附加的文本,而是在例程结束时查看所有操作。
半伪码:
RichTextBox richTextBox = new RichTextBox()
if (Operation1())
{
richTextBox.AppendText("Operation1 finished");
if (Operation2())
{
richTextBox.AppendText("Operation2 finished");
if (Operation3())
{
richTextBox.AppendText("Operation3 finished");
}
}
}
问题是,我查看操作1&2在操作3完成之后。
我在某个地方读到我需要使用一种名为BackgroundWorker的东西???
使用BackgroundWorker,只需将后台工作放入DoWork
,并将更新放入RunWorkerCompleted
:
var bw1 = new BackgroundWorker();
var bw2 = new BackgroundWorker();
var bw3 = new BackgroundWorker();
bw1.DoWork += (sender, args) => args.Result = Operation1();
bw2.DoWork += (sender, args) => args.Result = Operation2();
bw3.DoWork += (sender, args) => args.Result = Operation2();
bw1.RunWorkerCompleted += (sender, args) => {
if ((bool)args.Result)
{
richTextBox.AppendText("Operation1 endedn");
bw2.RunWorkerAsync();
}
};
bw2.RunWorkerCompleted += (sender, args) => {
if ((bool)args.Result)
{
richTextBox.AppendText("Operation2 endedn");
bw3.RunWorkerAsync();
}
};
bw3.RunWorkerCompleted += (sender, args) => {
if ((bool)args.Result)
{
richTextBox.AppendText("Operation3 endedn");
}
};
bw1.RunWorkerAsync();
你会注意到这与"DRY"相冲突。您总是可以考虑使用以下内容来抽象每个步骤的任务:
var operations = new Func<bool>[] { Operation1, Operation2, Operation3, };
var workers = new BackgroundWorker[operations.Length];
for (int i = 0; i < operations.Length; i++)
{
int locali = i; // avoid modified closure
var bw = new BackgroundWorker();
bw.DoWork += (sender, args) => args.Result = operations[locali]();
bw.RunWorkerCompleted += (sender, args) =>
{
txt.Text = string.Format("Operation{0} endedn", locali+1);
if (locali < operations.Length - 1)
workers[locali + 1].RunWorkerAsync();
};
workers[locali] = bw;
}
workers[0].RunWorkerAsync();
您可以执行以上3次,或者使用ReportProgress
在一个后台线程中运行所有任务,并定期报告进度
WPF(和大多数其他UI框架)的工作方式是有一个UI线程,它处理所有的UI事件(如按钮单击)和UI绘图。
如果UI忙于做其他事情,它就无法绘制内容。现在的情况是:
- 你点击一个按钮
- UI线程获得一条按钮单击消息,并调用您的单击处理程序函数
- 现在,在单击处理程序函数完成之前,UI无法重新绘制或执行任何其他更新
- Operation1函数完成,并将其附加到
RichTextBox
- UI无法更新,因为它仍然无法运行您的代码
- Operation2函数完成,并将其附加到
RichTextBox
- UI无法更新,因为它仍然无法运行您的代码
- Operation3函数完成,并将其附加到
RichTextBox
- 您的函数完成了,现在UI线程是免费的,它终于可以处理更新并重新绘制自己了
这就是为什么你会看到一个暂停,然后所有3个更新在一起。
你需要做的是让需要很长时间的代码在另一个线程上运行,这样UI线程就可以在你想要的时候自由地重新绘制和更新。这个示例程序对我很有用-它需要.NET 4.5来编译和运行
using System.Threading.Tasks;
...
// note we need to declare the method async as well
public async void Button1_Click(object sender, EventArgs args)
{
if (await Task.Run(new Func<bool>(Operation1)))
{
richTextBox.AppendText("Operation1 finished");
if (await Task.Run(new Func<bool>(Operation2)))
{
richTextBox.AppendText("Operation2 finished");
if (await Task.Run(new Func<bool>(Operation3)))
{
richTextBox.AppendText("Operation3 finished");
}
}
}
}
这里发生的是,我们使用了C#神奇的async
功能,操作顺序如下:
- 你点击一个按钮
- UI线程获得一条按钮单击消息,并调用您的单击处理程序函数
- 我们不直接调用
Operation1
,而是将其传递给Task.Run
。此助手函数将在线程池线程上运行Operation1
方法 - 我们使用magic
await
关键字来等待线程池完成operation1的执行。这在幕后所做的事情在道德上与此相当:- 暂停当前函数-,从而释放UI线程来重新绘制自己
- 当我们等待的事情完成时继续
因为我们现在正在线程池中运行长操作,所以UI线程可以在需要时绘制它的更新,并且您会看到消息如您所期望的那样被添加。
不过,这也有一些潜在的缺点:
- 因为
Operation1
方法没有在UI线程上运行,所以如果它需要访问任何与UI相关的数据(例如,如果它想从文本框中读取一些文本等),它就不能再这样做了。您必须首先完成所有UI内容,并将其作为参数传递给Operation1方法 - 通常,将需要很长时间(超过100ms)的东西放入线程池不是一个好主意,因为线程池可以用于其他事情(如网络操作等),并且通常需要一些空闲容量。如果你的应用程序只是一个简单的GUI应用程序,这不太可能影响你
如果这对您来说是个问题,您可以使用await Task.Factory.StartNew<bool>(_ => Operation1(), null, TaskCreationOptions.LongRunning)))
,每个任务都将在自己的线程中运行,不再使用线程池。不过它有点丑:-)