Introduction

When interfacing to real-world devices, it is typical to have one or more background processing tasks responsible for collecting data and/or controlling the devices. Monitoring these tasks can be done in a variety of ways, some of which can involve complex GUI components for status reporting and user interaction for control. As the name implies, the LogString class provides only a subset of this type of functionality.

There are many logging frameworks and APIs available, along with tools to deal with the Windows Event Log. I counted no less than 30 related CodeProject articles. While you could possibly use some of these tools, their functionality is not generally applicable to the requirements described below. That said, I should note that the project LogString was developed for uses log4net [^] for its system logging. The difference, of course, is that the two logging facilities (log4net and LogString) are used for completely different purposes.

This article also details how I used some basic .NET 2.0 Framework capabilities in the solution. There are no tricks or undocumented system features here. You can think of this as a C# 2 beginner's tutorial that shows some of the language features that would be useful in your day-to-day development work.

Requirements

The development of the LogString class came about because the application needed the ability for the user to monitor the activity of multiple background processing tasks in real-time. In other words, they needed to be able to view processing events as they happened. This is shown here:

The specific requirements are:

Multiple application-based log strings ("Task-1", "Task-2", etc.).

Each application log is relatively low volume. For my purposes, this meant that there are typically no more than 1 or 2 log events every couple of minutes.

The total size of a logging string is limited to the last 750-1000 event entries. Older events are not needed and are permanently discarded. [Note: This is the current LogString behavior, but a more practical implementation will have the ability to maintain a much longer log history.]

If true, add new entries to the start of the log. This makes viewing real-time updates easier because new items appear at the top while older entries scroll off the bottom. If set to false, new entries are appended to the end of the log text.

true

bool

LineTerminate

Terminate each new entry with CRLF.

true

bool

TimeStamp

Add timestamp to each log entry. If false, no timestamp is added.

true

int

MaxChars

Maximum number of characters allowed in the log. Once the log string reaches this size, the oldest text is removed: From the end if ReverseOrder is true, from the start if ReverseOrder is false.

32000

The GetLogString() method is used to access a named LogString instance:

LogString myLogger = LogString.GetLogString("Task1");

The returned object is a singleton. A background processing task would add a log entry by calling the Add() method:

myLogger.Add("Something important happened!");

A monitoring task accesses the entire contents of the log though the Log property. If textBox1 is a TextBox component, the log contents would be viewed with:

textBox1.Text = myLogger.Log

Automatic updates to a viewer are achieved by adding a LogUpdateDelegate delegate to the OnLogUpdate event of a LogString instance:

The LogUpdate function updates the TextBox component through the Invoke() method. These details are discussed below.

Each log string can be individually persisted with the Persist() instance method, but it would be more typical to use the PersistAll() static method when the application exits (or e.g. at timed intervals) to persist all logs with a single call:

LogString.PersistAll();

An individual log can be cleared with the Clear() method or all logs with ClearAll().

The LogStringTestApp project, which includes the LogString class, demonstrates most of this functionality, including logging from background threads.

Each LogString instance returned is a singleton. LogString instances can be removed from the table with the RemoveLogString method. This would only need to be done if you were creating many uniquely named logs so that the table would not fill up with unused instances.

Notice that the LogString constructor reads its log file (if it exists). This will automatically restore the contents of the named log string when it is instantiated.

Access Locking

In order to provide thread safe operation, whenever the internal log string is read or modified, it must be locked.

When one thread is inside the lock code block, another thread that executes a lock on the same object will be blocked until the other thread exits their block. I have used the C# lock syntax here instead of the more general purpose Mutex (or Monitor). There are several reasons for this:

I do not need cross-application resource locking, which is what a Mutex/Monitor class can provide.

The lock code is cleaner. You do not have to surround the resource use code with WaitOne()/ReleaseMutex() calls.

You don't have to worry about exceptions being thrown in the locked code block. The lock statement uses try..catch..finally to ensure that the lock is properly released. As such, you can also call return from inside a lock code block.

The Invoke() call is necessary so that the UI component updates can occur from other threads. The Anonymous delegate shown here is a new .NET 2.0 feature (see C#2 Anonymous Methods [^]) . Not having to define an extra delegate method makes the code cleaner and easier to follow.

Conclusion

If you have requirements that are close to what I've outlined, then you can use the LogString class as is or modify it for your own purposes. Many enhancements are possible. The C# language tools described are fairly basic and should be in your everyday tool box. Enjoy!