Redirect I/O to a TextBoxWriter in .NET
A data stream is like a river where you set data adrift. You put data on the river and it floats along, waiting to be plucked out at some other location. As with streams in nature, you can move up or down the stream, see what's there, and put in or take out things. Streams in .NET work that way too. In .NET, the console is represented by the stream class/metaphor. When you write a statement such as Console.WriteLine, the argument to WriteLine is put on the stream and it usually shows up in a command window.
Sometimes, though, you want basic output to go somewhere else besides the console (or DOS prompt). For example, System.Diagnostics sends Debug statements to the Output window in Visual Studio, and NUnit redirects Console.WriteLine statements to an internal window in its NUnit GUI. Well, you can redirect console output for your applications too, and this article shows you how. Specifically, you'll learn how to redirect Console output streams to a TextBox instead of the command window.
Creating a Custom TextWriter
The System.Console class has an Out property. The Out property is an instance of a TextWriter. To redirect Console output statements, you have to define a class that inherits from System.IO.TextWriter and replaces the default value of Out to an instance of your custom class. After you define and create an instance of your custom TextWriter, you replace the value of Out by calling Console.SetOut.
After defining the custom TextWriter class, you need to define two things:
- A control that will be the new output locus
- An event handler that is fired when the Windows handle of the new output control is created. (You need the Windows handle because you can't perform tasks such as sending text to a TextBox until that TextBox's Windows handle is created.)
Listing 1 shows the custom TextWriter import statements, class header, and fields.
Listing 1: The Stub for the Custom TextWriter
Imports System.Windows.Forms Imports System.Text Public Class TextBoxWriter Inherits System.IO.TextWriter Private control As TextBoxBase Private Builder As StringBuilder End Class
Implementing the Constructor
You add the constructor to the code in Listing 1. The constructor accepts a TextBox as an argument to the constructor and wires up the HandleCreated event handler for the TextBox. Listing 2 is the code for passing a textbox control to the Sub New constructor and wiring up an event handler for the TextBox's HandleCreated event. (The entire TextBoxWriter class is listed at the end of the article.)
Listing 2: Pass a TextBox Control and Wire Up an Event Handler
Public Sub New(ByVal control As TextBox) Me.control = control AddHandler control.HandleCreated, _ New EventHandler(AddressOf OnHandleCreated) End Sub
You don't create the StringBuilder field in the constructor because you need it only temporarily: when data is sent to the TextBoxWriter and until the TextBox's HandleCreated event fires.
Buffering I/O Until the TextBox Is Created
You can send text to the Console by using Console.Write or Console.WriteLine before a TextBox can actually handle the text. So, you need to implement a buffering scheme that can store text sent to the TextBoxWriter until the TextBox's handle is created. Once the handle is created, you can flush the buffer and forward any additional text directly to the control. Listing 3 contains a combination of Write and WriteLine methods and private subroutines for buffering text. The Public Write and WriteLine methods support basic output behaviors, and the private methods manage buffering text until the real output target is created. (You add the code in Listing 3 to the TextBoxWriter class from Listing 1.)
Listing 3: Public Write and WriteLine Methods, and Private Methods
Public Overrides Sub Write(ByVal ch As Char) Write(ch.ToString()) End Sub Public Overrides Sub Write(ByVal s As String) If (control.IsHandleCreated) Then AppendText(s) Else BufferText(s) End If End Sub Public Overrides Sub WriteLine(ByVal s As String) Write(s + Environment.NewLine) End Sub Private Sub BufferText(ByVal s As String) If (Builder Is Nothing) Then Builder = New StringBuilder() End If Builder.Append(s) End Sub Private Sub AppendText(ByVal s As String) If (Builder Is Nothing = False) Then control.AppendText(Builder.ToString()) Builder = Nothing End If control.AppendText(s) End Sub
All Write invocations end up at Write(ByVal s as String). This Write statement checks the control to see whether the handle has been created. If the TextBox's handle is created, it forwards the text to the control by using AppendText. If the control's handle hasn't been created yet, it buffers the text in the StringBuilder using the BufferText method. (The StringBuilder is created on demand, if needed, when BufferText is called.)
Knowing When the TextBox's Handle Is Created
The last bit of code is wired to the TextBox's HandleCreated event property. Listing 4 demonstrates how you can flush the buffer and release the StringBuilder when the TextBox's handle is created.
Listing 4: OnHandleCreated Responds When the TextBox's Handle Is Created
Private Sub OnHandleCreated(ByVal sender As Object, _ ByVal e As EventArgs) If (Builder Is Nothing = False) Then control.AppendText(Builder.ToString()) Builder = Nothing End If End Sub