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

DiagnosticsTextBox: Log Window for WinForms

0.00/5 (No votes)
25 Jun 2020 1  
A reusable Windows Form text box control for capturing DEBUG and TRACE output
This article discusses the implementation of a reusable, hassle free customized text box for Windows Form create to capture DEBUG and TRACE output with minimum impact on performance and memory consumption.

Introduction

Do you need to add a log window for your WinForm application? We created an easy to use user control to capture traces from application with thread support.

The latest source code is available at GitHub.

Background

When working with application development with WinForms, most of the time, we will want to capture output from System.Diagnostics.Debug and System.Diagnostics.Trace during development as well as after released, where the log is useful to understand the issue reported by user. The need increases with the complexity of the application. For applications which perform complex processing, a log window is a quick and useful indicator to tell the user that the application is running, not freezed so they don't just kill it.

Hence, we decided to create a custom control where we can just drag and drop to form to start logging immediately every time we start a new project without rewrite or copy a single line of code. These controls are named as DiagnosticsTextBox and DiagnosticsRichTextBox which is part of the CodeArtEng.Diagnostics NuGet package.

Using the Control

The control is released in GitHub and published in NuGet.

  • Include NuGet package CodeArtEng.Diagnostics to your WinForm project.
  • Drag DiagnosticsTextBox or DiagnosticsRichTextBox to your project.

Architecture

DiagnosticsTextBox and DiagnosticsRichTextBox are created with same design. We will describe the implementation based on DiagnosticsTextBox which shows plain text and describes additional methods and features included with DiagnosticsRichTextBox which support color formatting.

Image 1

Classes Overview

  • System.Diagnostcis.TraceListener: Provides the abstract base class for the listeners who monitor trace and debug output.
  • CodeArtEng.Diagnostics.TraceLogger: Derived from TraceListener, provide implementation of Write and WriteLine methods. Provide callback function for DiagnosticsTextBox.
  • CodeArtEng.Diagnostics.TraceFileWritter: Derived from TraceListener, implement logging to file. This class can also be used on it own.
  • CodeArtEng.Diagnostics.DiagnosticsTextBox: TextBox control to display DEBUG and TRACE messages.

Behind the Scenes

Capture and Parsing Message

TraceLogger is handling all the message received from TraceListener, process and pass it over to DiagnosticsTextBox and TraceFileWritter. Three callback functions implemented in TraceLogger: OnMessageReceived, OnWrite, OnFlush.

Class TraceLogger

public override void Write(string message)
{
    OnMessageReceived(ref message);
    if (string.IsNullOrEmpty(message)) return;

    message = ParseMessage(message);
    OnWrite(message);
    IsNewLine = false;
}

Class DiagnosticsTextBox

private void Tracer_OnMessageReceived(ref string message)
{
    //Message filter implementation.
    if(MessageReceived != null)
    {
        TextEventArgs eArg = new TextEventArgs() { Message = message };
        MessageReceived.Invoke(this, eArg);
        message = eArg.Message;
    }
}

/// <summary>
/// Occur when message is received by Trace Listener.
/// </summary>
[DisplayName("MessageReceived")]
public event EventHandler<TextEventArgs> MessageReceived;

When message is received from Write or WriteLine, the raw message is forwarded to parent class by OnMessageReceived. In DiagnosticsTextBox class, the message is then forward to the next level by MessageReceived event. Developer can make use of this event to react or filter message when necessary.

The message is then handled by ParseMessage to perform formatting of each and every incoming messages in Write and WriteLine methods before writing it to file or text box.

private string ParseMessage(string message)
{
    string dateTimeStr = ShowTimeStamp ? AppendDateTime() : string.Empty;
    string result = IsNewLine ? dateTimeStr : string.Empty;

    if (message.Contains("\r") || message.Contains("\n"))
    {
        //Unified CR, CRLF, LFCR, LF
        message = message.Replace("\n", "\r");
        message = message.Replace("\r\r", "\r");

        string newLineFiller = new string(' ', dateTimeStr.Length);
        string[] multiLineMessage = message.Split('\r');
        result += multiLineMessage[0].Trim() + NewLineDelimiter;
        foreach (string msg in multiLineMessage.Skip(1))
            result += newLineFiller + msg.Trim() + NewLineDelimiter;

        result = result.TrimEnd();
    }
    else result += message;
    return result;
}

If ShowTimeStamp is enabled, time stamp will be appended to message when ParseMessage is called. The format of the timestamp is defined in TimeStampFormat properties using the .NET string format.

private string AppendDateTime()
{
    switch (TimeStampStyle)
    {
        case TraceTimeStampStyle.DateTimeString: 
             return "[" + DateTime.Now.ToString(TimeStampFormat) + "] ";
        case TraceTimeStampStyle.TickCount: return "[" + DateTime.Now.Ticks.ToString() + "] ";
    }
    return "-";
}

Besides, ParseMessage also takes care of alignment of multi-line messages when time stamp is added. Text alignment is done by insert leading space by counting characters occupied by time stamp. The alignment works best with fixed width font, such as Courier New.

Image 2

Display Messages in DiagnosticsTextBox

We want to have the text box updated with latest messages as soon as possible but not blocking the Main thread until Main Form become not responding. Considering that Write and WriteLine will be called from either Main or Worker thread, we can't write the message to TextBox directly which can only be updated by MainThread.

A MessageBuffer is introduced in DiagnosticsTextBox to capture all incoming messages and update to text box control at defined timer interval, value configurable by RefreshInterval property. Every time when timer ticks, the text box is updated with messages from MessageBuffer. A lock is added to prevent MessageBuffer being modified simultaneously.

private void refreshTimer_Tick(object sender, EventArgs e)
{
    lock (LockObject)
    {
        //Transfer from Message Buffer to Diagnostics Text Box without locking main thread.
        if (MessageBuffer.Length == 0) return;
        this.AppendText(MessageBuffer);
        MessageBuffer = "";

Memory Consumption

One issue we encountered when we deployed the first revision of the DiagnosticTextBox is memory consumption increased overtime. For application which runs for hours and days, it eventually triggers Memory overflow exception. We then noticed that this was caused by the text box which had been filled up by all the log messages.

To improve the performance and minimize total memory consumption, we added a property named DisplayBufferSize to define maximum lines to display in text box. This is part of the refreshTimer_Tick method. We use Array.Copy to maximize the performance. User can still choose to set DisplayBufferSize to 0 to always display all messages.

        if (DisplayBufferSize <= 0) return;

        if (Lines.Length > DisplayBufferSize)
        {
            string[] data = new string[DisplayBufferSize];
            Array.Copy(Lines, Lines.Length - DisplayBufferSize, data, 0, DisplayBufferSize);
            Lines = data;
        }
        SelectionStart = Text.Length;
        ScrollToCaret();
    }
}

Formatting with Color

Image 3

The difference between DiagnosticsTextBox and DiagnosticsRichTextBox is the later one "Rich" in color. An additional method named AddFormattingRule to define font color of specific line based on matching string in a dictionary.

public void AddFormattingRule(string containString, Color color)
{
    if (SyntaxTable.ContainsKey(containString)) return;
    SyntaxTable.Add(containString, color);
}

Whenever TextChanged event trigger, the new added lines is scanned and format based on formatting rules. For each line, this might not be the most efficient implementation, but it's simple and easily readable without the need to manipulate RTF format in rich text box directly.

Notice that we use LastSelection to keep track of last updated line in such that we can skip lines which we already processed on next TextChanged event. Besides, an Updating flag is used to prevent recursive call when text box content is being updated in FormatText method.

private void DiagnosticsRichTextBox_TextChanged(object sender, EventArgs e)
{
    FormatText();
}

private void FormatText()
{
    if (Updating) return;

    Updating = true;
    try
    {
        int startLine = GetLineFromCharIndex(LastSelection);
        for (int x = startLine; x < Lines.Length; x++)
        {
            int lineStart = GetFirstCharIndexFromLine(x);
            int lineEnd = GetFirstCharIndexFromLine(x + 1) - 1;
            SelectionStart = lineStart; 
            SelectionLength = lineEnd < 0 ? 0 : lineEnd - lineStart + 1;

            if (AutoResetFormat)
                SelectionColor = LastFontColor = ForeColor;
            else
                SelectionColor = LastFontColor;

            foreach (KeyValuePair<string, Color> entry in SyntaxTable)
            {
                if (Lines[x].Contains(entry.Key))
                {
                    SelectionColor = LastFontColor = entry.Value;
                    break;
                }
            }
        }
        SelectionStart = LastSelection = Text.Length;
        ScrollToCaret();
    }
    finally { Updating = false; }
}

Unfortunately, there is no easy way to remove lines from rich text box without affecting it previous formatting. When number of lines in rich text box reached defined DisplayBufferSize, the entire rich text box had to be scanned and update format once again.

if (DisplayBufferSize <= 0) return;

if (Lines.Length > DisplayBufferSize)
{
    string[] data = new string[DisplayBufferSize];
    Array.Copy(Lines, Lines.Length - DisplayBufferSize, data, 0, DisplayBufferSize);
    Lines = data;
    LastSelection = 0;
    FormatText();
}

Points of Interest

One challenge we faced in developing this control is we can't call Write and WriteLine in any of the classes which will caused recursive call. We can only make use of breakpoint in Visual Studio to step through the code to identify the bug. We hope that this tool will benefit all fellow developers who work with WinForms for faster development and debugging.

History

  • 25th June, 2020: Initial version

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