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:
using Elements;
private ConsoleWriter m_ConsoleWriter;
public ConsoleWriter MyConsoleWriter
{
get
{
return m_ConsoleWriter;
}
private set
{
m_ConsoleWriter = value;
}
}
public class MyForm()
{
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 string
s 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:
...
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.
...
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.
...
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