0

I have a BackgroundWorker that runs a job generating a large amount of text.

When it's complete, I need it to execute an Async/Await Task Method, which writes and colorizes the text in a RichTextBox.

The Async/Await Task is to prevent the MainWindow UI thread from freezing while work is being calculated, such as searching and colorizing, for the RichTextBox.


Error

Exception: "The calling thread cannot access this object because a differnt thread owns it."

I get this error unless I put the Async/Await code inside a Dispatcher.Invoke.

But using a Dispatcher.Invoke seems to negate the Async/Await and cause the MainWindow UI thread to freeze.


C#

 public void Generate() 
 {
    // Background Worker
    //
    BackgroundWorker bw = new BackgroundWorker();
    bw.WorkerSupportsCancellation = true;
    bw.WorkerReportsProgress = true;

    bw.DoWork += new DoWorkEventHandler(delegate (object o, DoWorkEventArgs args)
    {
        BackgroundWorker b = o as BackgroundWorker;

        // Generate some text
        // ...
    });

    // When Background Worker Completes Job
    //
    bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(delegate (object o, RunWorkerCompletedEventArgs args)
    {
        // Write and Colorize text in RichTextBox
        Task<int> task = Display();

        bw.CancelAsync();
        bw.Dispose();
    });

    bw.RunWorkerAsync();
}


// Method that Writes and Colorizes text in RichTextBox in MainWindow UI
// 
public async Task<int> Display()
{
    int count = 0;
    await Task.Run(() =>
    {
        // Problem here, it will only work inside a Disptacher
        //Dispatcher.Invoke(new Action(delegate {
            // Write text
            Paragraph paragraph = new Paragraph();
            richTextBox1.Document = new FlowDocument(paragraph);
            richTextBox1.BeginChange();
            paragraph.Inlines.Add(new Run("Test"));
            richTextBox1.EndChange();

            // Colorize Text here...
            // Is a loop that takes some time.
            // MainWindow UI freezes until it's complete.
        //}));
    });

    return count;
}
Matt McManis
  • 4,475
  • 5
  • 38
  • 93
  • 1
    I doubt the background worker captures the right synchronization context. You're mixing the "old world" and the "new world". I would use `Task.Run` to start an additional thread for your "background worker code", and then use async/await/tasks all the way down (turtles all the way down baby...) – Lasse V. Karlsen Apr 14 '20 at 20:44
  • 2
    If you learn async/await, you should then get rid of `BackgroundWorker` completely. – Lex Li Apr 14 '20 at 20:58
  • BTW who's awaiting on the results of Display task? Did you intend to have "fire-and-forget" logic there? – Roman Polunin Apr 14 '20 at 21:02
  • @LexLi But I need a thread to process heavy work, and async/await to display it. But async/await is not a thread right? – Matt McManis Apr 14 '20 at 21:03
  • 2
    I don't think the problem is mixing "old world" and "new world", it's that you can't call methods/properties on a RichTextBox on anything but the main thread. And the BackgroundWorker does resume on the original synchronization context. – Darryl Apr 14 '20 at 21:04
  • I don't have experience with RichTextBox controls, so maybe this isn't a good suggestion, but have you tried making all the changes to the Paragraph object inside Task.Run(), but not assigning the rtb.Document until after Task.Run()'s await finishes? – Darryl Apr 14 '20 at 21:07
  • `RunWorkerCompleted` (and therefore `Display`) runs in the UI thread. I don't understand why you're using `Task.Run` there. The way BackgroundWorker works is: do the heavy computations in the background in the `DoWork` handler, and display the results in `RunWorkerCompleted` – Kevin Gosse Apr 14 '20 at 21:15
  • I'm removing the `BackgroundWorker` and trying to do everything with `Task.Run` and `ContinueWith`. https://pastebin.com/raw/0WCe20G8 But I'm still having to run `Dispatcher` for the `RichTextBox` and it freezes the UI. – Matt McManis Apr 14 '20 at 21:26
  • Two possible approaches: use a worker thread to build up an object that stores the results of your formatting loop, then use that object to quickly instantiate a FlowDocument in the UI thread. Or 2), dispatch individual formatting operations back to the UI thread instead of dispatching the whole loop. Not sure if either of those will help, just ideas. – Darryl Apr 14 '20 at 21:58
  • I give you a complete example on how to get rid of `BackgroundWorker`, https://github.com/lextm/backgroundworker-sample/commit/2e4cdf37c14b4e049407ea91db82dbefb125cc64 You might follow that to convert your own project. If anything is not clear to you, you can open an issue there. – Lex Li Apr 14 '20 at 23:20
  • There's a lot wrong with the code you posted, but ultimately the issue is simple: you have run code in a thread other than the UI thread (i.e. using `Task.Run()`) and failed to use `Dispatcher.Invoke()` to ensure code that uses UI objects is executed on the UI thread. The fact that you've commented out the part of the code that might have solved the problem doesn't change the fact that that's the part that's missing. Beyond that, the question is way too broad, as you have several architectural problems that would require a ton of back and forth to resolve. – Peter Duniho Apr 15 '20 at 00:36

1 Answers1

4

I agree with others that the code would be cleaner once you replace BackgroundWorker with Task.Run. Among other things, it's much easier to compose with await (by "compose", I mean "do this thing; then do this other thing"). Note that you should use await instead of ContinueWith.

So your original code would end up looking something like this once the BGW is converted to Task.Run:

public string GenerateSomeText(CancellationToken token)
{
  // Generate some text
}

public async Task GenerateAsync() 
{
  var cts = new CancellationTokenSource();
  var result = await Task.Run(() => GenerateSomeText(cts.Token));
  await DisplayAsync(result);
}

So, on to the issue that prompted this question in the first place: how to do lots of UI work without blocking the UI? Well, there isn't a great solution, because the work is UI work. So it can't be put on a background thread. If you have tons of UI work to do, the only real options are:

  1. Virtualize your data. This way you only need to process the amount of UI you are displaying. This is the best solution to this problem.
  2. If you don't want to put in the work to virtualize your data, then you can put in a hack where your code periodically pauses the UI work so that the UI remains responsive.

I do not believe the WPF RichTextBox supports virtualization, so you may need to go third-party for that. If you wanted to do the pause hack instead, you could do something like this:

public async Task<int> DisplayAsync(string text)
{
  int count = 0;

  // Write text
  Paragraph paragraph = new Paragraph();
  richTextBox1.Document = new FlowDocument(paragraph);
  richTextBox1.BeginChange();
  paragraph.Inlines.Add(new Run(text));
  richTextBox1.EndChange();

  // Colorize Text here...
  // Is a loop that takes some time.
  for (loop)
  {
    ... // Colorize piece of text.
    await Task.Delay(TimeSpan.FromMilliseconds(20));
  }

  return count;
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    Is there any reasoning behind 20ms, or is it just arbitrary? Any difference between that and `await Task.Yield()` for this purpose? – Gabriel Luci Apr 15 '20 at 02:00
  • 1
    It's an arbitrary value meant to be "enough time for the UI to catch up on its messages but not so much time that it will be a noticeable slowdown overall". `Task.Yield` won't work here because the Win32 message queue is a prioritized message queue, and `Task.Yield` will *immediately* queue the continuation, so when the winproc gets its next message, it will get the continuation (a higher priority message) and not see its pending messages (such as paint, a lower priority message). – Stephen Cleary Apr 15 '20 at 13:31