Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ConsoleWriter

0.00/5 (No votes)
12 Apr 2010 1  
A control for writing out text to the UI

Introduction

It's sometimes useful to be able to relay information back to the user with line by line text. The need is to provide a listing of information forming a trace. An example most developers will be familiar with is the Visual Studio 'output view pane' during build, load, 'find results', etc.

A common approach to building such a control is to modify one of the common controls, but I felt it would be much more convenient to have something ready to use and built for the task, so I decided to fill the gap in my toolbox and write one based on UserControl. It's a fundamental interface device that predates the GUI control and it continues to be an interesting and enjoyable development task.

This first major revision to the control, which principally adds support for cut and paste, also includes an improved demonstration program and a background event sink helper for building a more robust and responsive console host. This might be of interest to those building server consoles because this type of program can be running for considerable periods of time and confidence in a controls ability to manage resources, especially one that might be involved in a lot of activity, is important. Refer to the Sample Application section for more details.

Future plans are to include persistence, with save log to file and automatic truncate log to file.

Those interested in control development might find some of the control code useful. In this respect, I have demonstrated using AutoScroll combined with painting text, etc. The code may be helpful to those learning to use the AutoScroll feature. It also demonstrates an implementation of Cut and Paste functionality. Those wanting to manipulate strings via mouse or keyboard will hopefully find the source code useful.

Creating and Using the ConsoleWriter Control

To incorporate the control into your C# Forms Project, add the file ConsoleWriter.cs to your project. You can refer to Elements.ConsoleWriter or add using Elements; at the top of your code file to be able to refer to just the class.

To code your own ConsoleWriter member, you can do something like:

//MyForm.cs
using Elements;

//declare a variable of type ConsoleWriter
private ConsoleWriter m_ConsoleWriter;

//provide a property accessor (optional)
public ConsoleWriter MyConsoleWriter
{
	get
	{
		return m_ConsoleWriter;
	}
	private set
	{
		m_ConsoleWriter = value;
	}
}

public class MyForm()
{
	//create and add it to the controls
	MyConsoleWriter = new ConsoleWriter();
	MyConsoleWriter.Dock = DockStyle.Fill;
	Controls.Add(MyConsoleWriter);
}

If you use form designers, you should find the ConsoleWriter available in the Toolbox once it is added to your project. You can drag it onto your design surface from there.

ConsoleWriter Methods

The control derives from UserControl so the inherited functionality is applicable to the ConsoleWriter where relevant.

There are four methods exposed by the ConsoleWriter itself:

  • Add(string text): This takes a string parameter and is the text you want to output to the ConsoleWriter.
    The principle behind this one means of adding output to the control is that the user can either send in one line of text, or text that contains multiple lines. The Add function parses the input text for line feeds and processes each of those lines as a new row. There are many ways to prepare strings as multiple lines with .NET. One of the most useful is StringBuilder from System.Text. This allows you to build up a string and includes a method AppendLine which will embed line feeds in the string output available from its ToString() method.
  • Clear(): This is a parameterless method that instructs ConsoleWriter to clear the console.
  • CopySelectedToClipboard(): This is a parameterless method that instructs ConsoleWriter to place selected text on the system clipboard.
  • SelectAll(): This is a parameterless method that instructs ConsoleWriter to select all of the text in the control.

ConsoleWriter Properties

There are three properties exposed by the ConsoleWriter:

  • LineBufferLimit: In order to be able to scroll back through previous output, the control retains a buffer of lines. You can set this to a positive integer value to suit your needs. The default value is 1000. If you set it to zero or below, then the buffer will grow unchecked. A call to Clear() will empty the buffer. The buffer is a SortedDictionary that holds all the information the control needs to paint the text from the current scroll position.
  • AutoRemoveCount: If there is a LineBufferLimit greater than zero, e.g. the default of 1000, when that limit is reached the value of AutoRemoveCount - the default is 250 - items are removed from the top of the buffer. Therefore in the default mode (1000/250), once the LineBufferLimit has been reached there are always between 750 and 1000 items in the buffer. If you have a LineBufferLimit greater than zero but an AutoRemoveCount of zero, then the buffer will behave like a FIFO queue with a fixed size once the limit is reached. This is not advised unless the effect is desired as the list can never enter a Stationary mode. (See the section headed Trailing for more about modes.)
  • BandColor: ConsoleWriter supports alternating colored lines, which appear as bands like listing paper if the chosen color is pale enough. The default color is SystemColors.ControlLight. The background color defaults to SystemColors.Window and the text color defaults to SystemColors.WindowText.

    If you want the optimization of no BandColor, e.g. a plain background, then set the BandColor to the BackColor value - those interested in the control code will note that the OnPaintBackground override of ConsoleWriter.cs checks for this equality and makes no attempt to paint bands of the same color.

    Set an alternative BandColor value if required. This code example sets the control to a plain display:

    //MyForm.cs
    ...
    MyConsoleWriter.BandColor = ConsoleWriter.BackColor;
    ...

Derived Color Properties

  • Use the BackColor property to set an alternative background color, and similarly set the ForeColor derived property to change the color of text.
    //MyForm.cs
    ...
    ConsoleWriter.BackColor = System.Drawing.Color.White;
    ConsoleWriter.BandColor = System.Drawing.Color.MintCream;
    ConsoleWriter.ForeColor = System.Drawing.Color.Black;
    ...
  • Font: The ConsoleWriter Font defaults to Courier New 8.25 a fixed character width font which provides a standard appearance and easily read output. You can choose any font for the control by setting the Font property at construction, e.g. for designer hosted composition, these properties are available in the properties task pane of Visual Studio.
    //MyForm.cs
    ...
    MyConsoleWriter.Font = new System.Drawing.Font("Arial", 11.25f,
        System.Drawing.FontStyle.Regular, 
    	System.Drawing.GraphicsUnit.Point, ((byte)(0)));
    ...

Trailing

The ConsoleWriter has two modes, either Stationary or Trailing. If Trailing the control keeps the latest output visible at the bottom of the screen, this means that the window is being scrolled automatically and the older lines beyond the display capacity of the ClientRectangle Height will scroll off screen. Those interested in how this is achieved using the functions related to AutoScroll can see this in the Controls InternalRefresh method. The AutoScrollMinSize property is checked and, if necessary, reset, and if the control is in Trailing mode the AutoScrollPosition is set to correspond with the last line, e.g. the most recent.

When Stationary, the control is at a fixed point in the AutoScroll DisplayRectangle - this allows the user of the program to scroll back through the current buffer of lines.

These features are reasonably intuitive to the control user. The control starts life in Trailing mode and will therefore show latest output and scroll automatically. If the user scrolls the display up at all, then the control will automatically enter the Stationary mode.

When the control is scrolled back down to the end of the list, then the Trailing mode will automatically be engaged again. Pressing the keys Ctrl+End will also re-engage the trailing end of the list.

Command Keys and Mouse Control

The control responds in a natural manner to scrolling using the mouse and scrollbar controls. The scroll arrows when pressed move the display by a line increment either up or down. Direct manipulation of the scroll box (thumb) moves the display with the standard scroll behaviour as does mouse clicking within the scrollbar shaft itself. The control also responds normally to mousewheel activity.

Text can be selected by holding the Mouse Left Button down where selection should start and moving to determine the extent of the selection. Releasing the mouse button ends the selection. The text remains selected until another mouse click is received.

Control developers can observe the OnScroll and OnMouseWheel overrides to see how this is implemented. You will note that the VerticalScroll.SmallChange is set to the LineHeight to ensure that the one line increment can be achieved.

When the mousewheel attempts to move the display beyond the end of the list, the control will automatically enter the Trailing mode. This is also the case for the scroll arrow. I have not implemented this behaviour when the thumb is moved to the end of the list because this allows the user to remain in Stationary mode when observing values at the end of the list. This is most appreciable when the control is rapidly receiving values.

The control also responds to the following command keys:

  • Ctrl-Home scrolls the display to top of the DisplayRectangle and leaves the control in Stationary mode.
  • Ctrl-End scrolls the display to the end of the DisplayRectangle and leaves the control in Trailing mode (A single click to the thumb will halt Trailing and enter Stationary mode).
  • PageUp and PageDown scroll the display by the display height. If the PageDown key attempts to move beyond the end of the list, the control will automatically enter the Trailing mode.
  • ArrowUp and ArrowDown scroll the display by the line height. If the ArrowDown key attempts to move beyond the end of the list, the control will automatically enter the Trailing mode.
  • Ctrl-A Selects all the text in the control.
  • Ctrl-C Copies any selected text to the ClipBoard.

Control developers can observe the ProcessCmdKey override to see how this is implemented.

Sample Application - ConsoleWriterSample.exe

To help you evaluate and understand how the ConsoleWriter works and behaves, I have created a sample MDI Application. Its sole purpose is to demonstrate one or more ConsoleWriters working.

To keep a constant supply of information available to the ConsoleWriter, the main form of the application has a timer. The child windows of this application all contain a ConsoleWriter and they can subscribe to a timer event published by the main form. When the main form receives the timers elapsed event, it broadcasts this event to the subscribing child windows and they in turn write this information to the ConsoleWriter. To provide further test output to the ConsoleWriter the child form also supports two commands. One is to list the current environment and the other to list the currently loaded modules.

You can alter the interval period of the main form timer via a dialog provided under the main forms options menu. You can alter the settings for the ConsoleWriter control programmatically in the ConsoleChild Create method.

In the original release of this control, I had placed the DoEvents method in the Add method with the intention of freeing up the UI when several ConsoleWriters were under high usage. This is because I found that whilst using the Sample Application with several ConsoleWriters all receiving messages at an intense rate, for example 100 millisecond intervals, the UI could be frozen or choked. On reflection DoEvents was not a good idea, as it can cause problems in the hosting program and I think it therefore undermines the integrity of a control. It didn't solve a bigger problem I encountered either - when disconnecting and disposing from a high speed event source. I got exceptions for attempted invocations on disposed objects, despite testing for Disposed and Disposing, and making several attempts to find a robust technique.

I eventually found the solution to both problems in a class I had developed a while back to host a thread in a convenient way. I used this thread hosting class to be an event sink for the Form1 timer event and an event source for the ConsoleWriter. This technique has cured both the UI problem and the event detachment problem. In my testing, I have not experienced any problems above 100 milliseconds (my testing uses 9 open ConsoleWriters receiving at this rate). The next section describes the Thread Class and how an application can use it to achieve a more robust ConsoleWriter setup.

The bitmaps used in the menus and buttons are public domain and available from www.famfamfam.com.

EventWaiter Thread Helper Class

EventWaiter is a base helper class from which you can inherit and write your own threadstart thread classes. Some might find this useful as a general purpose threading utility class.

The EventWaiter class is a completely optional class you can use to achieve a more robust console hosting environment when programming for extreme conditions, or defence, or when you think hosting an event sink on another thread would help your program.

It is particularly of use if, for example, there are going to be many consoles and they are going to be placed under high and sustained loads. Under such conditions, the EventWaiter class can help keep the UI thread available for user input and intervention, which is important with these type of programs. For instance a console might need to be shut down, but if the UI is not responding to allow the command to detach to be issued - then the program wouldn't be working properly or likely not responding at all.

The EventWaiter class provides you with a thread hosted sink for your external event source(s) and in turn an internal event source for your ConsoleWriter. This thread can be safely shut down allowing you to detach from the source before say, disposing the ConsoleWriter or attaching a new event source.

The example that follows explains the Sample Application usage of EventWaiter.

As explained in the Sample Application notes, each child MDI window (ConsoleChild), that hosts a ConsoleWriter, can subscribe to the timer event published by Form1. Instead of subscribing directly in the UI thread of our application, we use a class derived from the EventWaiter class to receive these events on a separate background thread.

In ConsoleChild.cs the event OnMenuItemSubscribeClick receives user menu instruction to start receiving events.

ConsoleChild has a member of type Form1EventWaiter and this is a thread event handler class derived from EventWaiter.

In OnMenuItemSubscribeClick, if Form1EventWaiter is null, then a subscription is created. The ConsoleChild helper method CreateForm1EventWaiter() is called to handle creating the thread event handler and starting its execution. In CreateForm1EventWaiter a Thread is created with thread start parameter of type EventWaiter.EventWaiterThreadStart. This is the method that is executed when the thread starts life.

In this example, the thread start parameter TypeString is set to "SampleConsoleWriter.Form1EventWaiter" (the EventWaiter derived class), the parameter EventWaiterEventHandler is set to the ConsoleChild handler - OnEventWaiterEvent, and the Tag parameter to ConsoleChilds MdiParent, which of course is Form1 - that provides the event ConsoleChild is to subscribe to.

If in OnMenuItemSubscribeClick Form1EventWaiter is not null, then a subscription to the event exists and therefore UnSubscribe is called. In UnSubscribe the Form1EventWaiter.DetachEventWaiterEvent is called with the OnEventWaiterEvent handler to detach.

Now take a look at OnEventWaiterEvent. The first obvious point is that the code in the handler does not deal directly with the event. That's because our event arrives on a thread that's not the main UI thread. The event needs marshalling to the UI thread. This is done by 'Invoking' a method on the UI thread using a 'delegate' which constitutes an invocation signature. In this case HandleEventWaiterEvent is called with the event arguments.

HandleEventWaiterEvent is important, not just to handle the subscribed event on Form1, but to also handle some of the requirements in running the new thread.

In HandleEventWaiterEvent the eventWaiterEventArgs.Message is switched for its string value. The thread class sends the message "FirstRun" after initialization. The thread is ready to start so ConsoleWriter casts the sender object parameter to the Form1EventWaiter member and calls Form1EventWaiter.Wait(); and Form1EventWaiter.Go(); to start the events.

When UnSubscribe is called, the event is detached and the thread class message "Detached" is received in HandleEventWaiterEvent. This means that ConsoleWriter can now safely call Form1EventWaiter.Stop(); and Form1EventWaiter.Dispose();

The thread class message "Form1Event" was defined in the derived Form1EventWaiter class and is the Form1Event that has been subscribed to arriving. In this case, ConsoleWriter adds the message to its ConsoleWriter.

History

  • Version 1.0.0 Released: 8th January, 2009
  • Version 1.0.1 Released: 17th January, 2009 - Fixes to OnResize override code that could cause a refresh problem
  • Version 2.0.0 Released: 11th April, 2010 - Adds cut and paste functionality and background event sink helper

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here