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; } 
11
  • 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...) Commented Apr 14, 2020 at 20:44
  • 2
    If you learn async/await, you should then get rid of BackgroundWorker completely. Commented Apr 14, 2020 at 20:58
  • BTW who's awaiting on the results of Display task? Did you intend to have "fire-and-forget" logic there? Commented Apr 14, 2020 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? Commented Apr 14, 2020 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. Commented Apr 14, 2020 at 21:04

1 Answer 1

5

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; } 
Sign up to request clarification or add additional context in comments.

2 Comments

Is there any reasoning behind 20ms, or is it just arbitrary? Any difference between that and await Task.Yield() for this purpose?
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).

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.