Updating your RichTextBox safely


I’ve gotten a few more questions about this after the brief mention about the RichTextBox threading problem.



  • Is it just a rich text box problem?  Can I use a TextBox instead?

    • No, unfortunately all windows forms UI, as well as most Win32 UI is not thread safe. 

  • Can I workaround it by pumping messages to eliminate the race condition?

    • Unfortunately that is not a guaranteed method to solve the race condition.

What to do!?  There’s a really simple pattern for updating the UI from another thread, it really isnt as complicated as you might imagine.


Calling back on the right thread, the dirt-simple way


If you have nothing to pass back from the other thread, it is really simple.


Using our System.Timers.Timer Elapsed event (which we know calls back on a different thread), we’re going to try to update our RichTextBox.  We’ve called the function that is going to update the text of the rich textbox UpdateRichTextBox(). 

private void UpdateRichTextBox() {
      string messageToLog = “Timer ticked at: ”  + DateTime.Now.ToString() + “\r\n”;
      richTextBox1.AppendText(messageToLog);
}


From the previous article, you know that directly calling on RichTextBox.Text or RichTextBox.AppendText() is a bad idea – so we’re going to be careful about how we exactly call UpdateRichTextBox. 


Our modus operandi is to:


1. Check if we’re on the wrong thread by using the InvokeRequired property.
2. If InvokeRequired returns true, either call Invoke or BeginInvoke instead of directly calling back on the method.


   private void nonThreadSafeTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) {
        if (richTextBox1.InvokeRequired) {
                // we are currently on the wrong thread.
                // use Invoke to call UpdateRichTextBox on the correct thread.
                richTextBox1.Invoke(new MethodInvoker(UpdateRichTextBox));
               
            }
            else {
                // we are on the right thread, we can directly call UpdateRichTextBox
                UpdateRichTextBox();
            }
    }


Note the use of MethodInvoker.  MethodInvoker is just a void delegate with no arguments, which means that you can call any method you like as long as it returns void and has no arguments.  This is great – a really simple way of calling a method cross-thread, but we havent really returned any data from the other thread.  We’ll get to that in a minute.


Getting just a bit fancier, fixing UpdateRichTextBox so its always Thread Safe 


There’s one other thing that kindof stinks about this solution.  Everywhere you call UpdateRichTextBox, you have to wrap it in the InvokeRequired check.  Lets see if we can fix that.

        private void UpdateRichTextBox() {
            if (richTextBox1.InvokeRequired) {


                // call back on this same method, but in a different thread.
                richTextBox1.Invoke(new MethodInvoker(UpdateRichTextBox));
            }
            else {
                // you are in this method on the correct thread.
                string messageToLog = “Timer ticked at: ”  + DateTime.Now.ToString() + “\r\n”;
                richTextBox1.AppendText(messageToLog);
            }
        }


        private void nonThreadSafeTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) {
            UpdateRichTextBox();
        }

By putting the check within UpdateRichTextBox, we’ve just ensured that anyone can call it from any thread.  This pitfall of this solution is that UpdateRichTextBox has to be carefully written as it is being called re-entrantly on two different threads.


Getting even more fancy, passing back custom data from the other thread.


Using MethodInvoker, you can only call void functions with no arguments.  This isn’t really helpful for passing back custom data to the control.  A common scenario is to use the rich text box to log some network activity.  In order to do this, you need to pass back some sort of message from the thread that is communicating with the network.


If we create our own delegate, we can pass back any data we like.  The way to do this is to define your delegate (method signature)
        private delegate void RichTextBoxUpdateEventHandler(string message);


Then change the Invoke from
richTextBox1.Invoke(new MethodInvoker(UpdateRichTextBox));


To
richTextBox1.Invoke(               
                    new RichTextBoxUpdateEventHandler(UpdateRichTextBox), // the method to call back on
                    new object[] {message});                              // the list of arguments to pass

Finally what we wind up with is


      private void UpdateRichTextBox(string message) {


            if (richTextBox1.InvokeRequired) {
                // this means we’re on the wrong thread! 
                // use BeginInvoke or Invoke to call back on the
                // correct thread.
                richTextBox1.Invoke(               
                    new RichTextBoxUpdateEventHandler(UpdateRichTextBox), // the method to call back on
                    new object[] {message});                              // the list of arguments to pass
            }
            else {
                richTextBox1.AppendText(message);
            }
        }


        private void nonThreadSafeTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) {
            string messageToLog = “Timer ticked at: ”  + DateTime.Now.ToString() + “\r\n”;
            UpdateRichTextBox(messageToLog);
        }


Summing up

This is just intended to get you started on your way to safe multithreading with windows forms.  All examples were with Invoke, which blocks the other thread (in our example the thread with the timer callback nonThreadSafeTimer_Elapsed).  If you dont want to stop the other thread from exexuting, use BeginInvoke to accomplish the work asynchronously.


There are plenty of other great articles out there, I’ll point you to three from the Wonders of Windows Forms series (Part 1) (Part 2) (Part 3) and the Background Worker component in Whidbey.



 

Comments (5)

  1. jfo's coding says:

    Why do we need both SystemEvents.UserPreferenceChanging and SystemEvents.UserPreferenceChanged?

    The…

  2. jfo's coding says:

    Custom PaintingPainting best practices ComboBox OwnerDrawLayoutDock layout/Using the Splitter control…

Skip to main content