2

What I am trying to achieve is to add text after every operation to a RichTextBox. The problem is, that these operations take some time and instead of viewing the appended text after every operation finishes, I view them all at the end of the routine.

Semi-Pseudo code:

RichTextBox richTextBox = new RichTextBox()

if (Operation1())
{
   richTextBox.AppendText("Operation1 finished");

   if (Operation2())
   {
      richTextBox.AppendText("Operation2 finished");

      if (Operation3())
      {
         richTextBox.AppendText("Operation3 finished");
      }
   }
}

The problem is that I view the appended text of operation 1 & 2 after the operation 3 is finished.

I read somewhere that I need to use something called BackgroundWorker???

McGarnagle
  • 101,349
  • 31
  • 229
  • 260
Erez
  • 37
  • 1
  • 8

2 Answers2

3

Using BackgroundWorker, you would just put the background work into DoWork, and the update into 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();

You'll notice that this runs afoul of "DRY". You could always consider abstracting the tasks for each step using something like:

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();

You could do the above 3 times, or use ReportProgress to run all tasks in one background thread, and periodically report progress.

McGarnagle
  • 101,349
  • 31
  • 229
  • 260
  • I can't manage to make my code work as you explained here. My operations are triggered when I press a button inside a WPF window (Button_Click event), the structure of the operations are in the form of: if(operation1 == true) then if (operation2 == true) then if (operation3 == true)... – Erez Dec 16 '13 at 22:03
  • @Erez ah, that's slightly different then. See my edit above. – McGarnagle Dec 16 '13 at 22:53
  • It worked! I also used [this](http://stackoverflow.com/questions/9732709/the-calling-thread-cannot-access-this-object-because-a-different-thread-owns-it) for invoking text appending. Thank you for your time! – Erez Dec 17 '13 at 13:58
2

The way that WPF (and most other UI frameworks work) is that there is a UI thread, which handles all the UI events (such as button clicking) and UI drawing.

The UI can't draw things if it's busy doing other things. What's happening is this:

  • You click a button
  • The UI thread gets a button click message, and invokes your click handler function
    • Now, the UI can't redraw or perform any other updates until your click handler function finishes.
  • Your Operation1 function finishes, and you append to the RichTextBox
    • The UI can't update because it's still stuck running your code
  • Your Operation2 function finishes, and you append to the RichTextBox
    • The UI can't update because it's still stuck running your code
  • Your Operation3 function finishes, and you append to the RichTextBox
    • Your function finishes, and now the UI thread is free, and it can finally process the updates and redraw itself.

This is why you see a pause and then all 3 updates together.

What you need to do is make the code that takes a long time run on a different thread so that the UI thread can remain free to redraw and update when you'd like it to. This sample program works for me - it requires .NET 4.5 to compile and run

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");
          }
       }
    }
}

What happens here is that we use the C# magical async feature, and the order of operations goes like this:

  • You click a button
  • The UI thread gets a button click message, and invokes your click handler function
  • Instead of calling Operation1 directly, we pass it to Task.Run. This helper function will run your Operation1 method on a thread pool thread.
  • We use the magic await keyword to wait for the thread pool to finish executing operation1. What this does behind the scenes is something morally equivalent to this:
    • suspend the current function - and thus free up the UI thread to re-draw itself
    • resume when the thing we're waiting for completes

Because we're running the long operations in the thread pool now, the UI thread can draw it's updates when it wants to, and you'll see the messages get added as you'd expect.

There are some potential drawbacks to this though:

  1. Because your Operation1 method is Not running on the UI thread, if it needs to access any UI related data (for example, if it wants to read some text from a textbox, etc), it can no longer do this. You have to do all the UI stuff first, and pass it as a parameter to the Operation1 method
  2. It's generally not a good idea to put things that take a long time (more than say 100ms) into the thread pool, as the thread pool can be used for other things (like network operations, etc) and often needs to have some free capacity for this. If your app is just a simple GUI app though, this is unlikely to affect you.
    If it is a problem for you, you can use the await Task.Factory.StartNew<bool>(_ => Operation1(), null, TaskCreationOptions.LongRunning))) instead and each task will run in it's own thread and not use the thread pool any more. It's a bit uglier though :-)
Orion Edwards
  • 121,657
  • 64
  • 239
  • 328
  • thanks for the reply. I'm getting this error: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'. What am I missing? – Erez Dec 17 '13 at 12:12
  • @Erez - as per the compiler error, you need to mark the method with the `async` modifier. I've updated my answer to show how you'd do this. You don't need to change it's return type to task though – Orion Edwards Dec 17 '13 at 21:15
  • Thanks, it worked. But what if the operation needs to get parameters? I'm getting "Method Name Expected" when trying to pass values to the method. – Erez Dec 18 '13 at 14:33
  • I can't edit my comment, but what I meant is how can I do this: `if (await Task.Run(new Func(Operation1(a,b)))) {}` where operation1 gets 2 variables as an input. – Erez Dec 18 '13 at 16:51
  • Managed to fix this issue using [this](http://stackoverflow.com/questions/10761712/no-overload-for-matches-delegate-system-action) – Erez Dec 19 '13 at 18:58