将文本添加到RichTextBox异步#C/WPF

本文关键字:WPF 异步 RichTextBox 文本 添加 | 更新日期: 2024-10-18 12:03:03

我试图实现的是在每次操作后向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的东西???

将文本添加到RichTextBox异步#C/WPF

使用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 ended'n");
        bw2.RunWorkerAsync();
    }
};
bw2.RunWorkerCompleted += (sender, args) => {
    if ((bool)args.Result)
    {
        richTextBox.AppendText("Operation2 ended'n");
        bw3.RunWorkerAsync();
    }
};
bw3.RunWorkerCompleted += (sender, args) => {
    if ((bool)args.Result)
    {
        richTextBox.AppendText("Operation3 ended'n");
    }
};
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} ended'n", 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线程可以在需要时绘制它的更新,并且您会看到消息如您所期望的那样被添加。

不过,这也有一些潜在的缺点:

  1. 因为Operation1方法没有在UI线程上运行,所以如果它需要访问任何与UI相关的数据(例如,如果它想从文本框中读取一些文本等),它就不能再这样做了。您必须首先完成所有UI内容,并将其作为参数传递给Operation1方法
  2. 通常,将需要很长时间(超过100ms)的东西放入线程池不是一个好主意,因为线程池可以用于其他事情(如网络操作等),并且通常需要一些空闲容量。如果你的应用程序只是一个简单的GUI应用程序,这不太可能影响你
    如果这对您来说是个问题,您可以使用await Task.Factory.StartNew<bool>(_ => Operation1(), null, TaskCreationOptions.LongRunning))),每个任务都将在自己的线程中运行,不再使用线程池。不过它有点丑:-)