Click here to Skip to main content
13,895,535 members
Click here to Skip to main content
Add your own
alternative version


59 bookmarked
Posted 29 Oct 2014
Licenced CPOL

Windows Services in C# with Timers - Jump start

, 26 Nov 2014
Rate this:
Please Sign up or sign in to vote.
Jump-start for the rapid implementation of a C# Windows Service supporting Pause/Continue, using single or multiple System.Net.Timer-based worker processes and Apache log4net logging


This is a jump start and example implementation for those who need to implement a C# Windows Service where the following requirements apply:

  • The incorporation of one or more independent worker processes within the Service, each driven by an underlying instance of System.Timers.Timer
  • Start, Stop, Restart, Pause and Resume/Continue calls from the Service Control Manager (Power Events are not included)
  • A basic Apache log4net logging implementation that can be further built upon
  • Built-in "plumbing" to handle Service state transitions, timer events, disposal of timers, and thread-safe communication between the Service component class and its workers.


You should be aware that there is some debate as to whether implementing Services with System.Timers.Timer is necessarily a good idea. I have found it to be a reliable method of implementing Services, but there is some dissent amongst respected professionals. For example, see:

A quick word on logging and debugging

Once you've decided that a timer-based Service is what you need, next accept the need for logging. If you want to build services, you will need to dedicate 5% (or more) of your coding efforts to logging. This can feel like a pain if you're coming at it from a desktop apps background, or from any other environment where logging is a secondary consideration, but if you are serious about building Services for production use, I assume the rationale as to the need for extensive logging requires no further explanation here.

Apache log4net is an excellent open-source logging library and I recommend you use it. There are alternatives, but this jumpstart and the code that accompanies it assumes that you are happy with this choice of logging library.

If you have no experience of debugging Windows Services, you should at the very least read both of these articles:

Debugging Windows Service projects can be a slightly awkward experience relative to, say, debugging a Console application. A suggested approach is that if you have lots of code that you need to debug and test, keep it in a Windows Library project. Test and debug your code using Visual Studio's unit test features, a Console Application or some other suitable project host type. After you have completed all (or at least, most) of your testing, you can then move that hardened, good-quality code across to a Windows Services project for the final run in. All other things being equal, you want to keep the amount of debugging and testing you have to do against a Windows Service to a minimum, but not to the detriment of code quality.

Keep in mind that Services must be Installed before they are debugged. Installing successfully with installutil can be a little troublesome for the novice. If you are new to this, do a little research before getting stucking in. It'll save you frustration later.

Jump start

Implementing concrete (derived) classes using the jump start code

The jump start code, encapsulated in the library ServiceTimerLib, consists of two key classes. The first of these is TimerServiceBase, which inherits from System.ServiceProcess.ServiceBase and the second is the disposable TimerWorker class.

You should think of both of these classes as abstract classes that you inherit from to construct the concrete classes of your Windows Service.

Note: In fact, the TimerServiceBase class is not implemented as an abstract class, because doing so "breaks" the Visual Studio Service Designer Component. Conceptually, however, both classes are abstract - the intention is that you should derive from them by way of inheritance to construct the concrete classes of your Service.

When a vanilla Windows Service project is created in Visual Studio, the automatically-generated Service component class inherits from System.ServiceProcess.ServiceBase. When using the jump start code, you change this so that the component inherits from TimerServiceBase instead. The required chain-of-inheritance remains unbroken because, as just mentioned, TimerServiceBase itself inherits from ServiceBase.

Diving straight in to the shallow end, let's show you a concrete implementation of a Service component class:

/// <summary>
/// The service class inherits from TimerServiceBase, rather than inheriting from ServiceBase
/// </summary>
public partial class PrimeCalcService : TimerServiceBase
    public PrimeCalcService()

    /// <summary>
    /// In OnStart, we need do litte more than construct our two worker classes, and
    ///  "register" them using the base class RegisterWorker() method. We also set up
    ///  the log4net logging
    /// </summary>
    /// <param name="args"></param>
    protected override void OnStart(string[] args)
        // Set up log4net logging. Note that this call by itself is enough to ensure that your
        //  worker classes also get an ILog object, provided this call is made prior to the
        //  RegisterWorker() calls. An additional caveat is that it is assumed you are satisfied
        //  with a call to log4net's XmlConfigurator - if you are using a different logging methodology,
        //  you'll need to hack some of the code to get exactly what you want

        LargePrimeWorker largeWorker = new LargePrimeWorker();
        SmallPrimeWorker smallWorker = new SmallPrimeWorker();


What you see in the block above is literally all of the code that is required to implement a functional Service component class using the jump start. Quite a lot more goes on, of course, but most of it is abstracted away in TimerServiceBase.

A few things happen in the OnStart method:

  • The inherited DefaultLog method is called. This does a little work to set up log4net logging.
  • An instance of LargePrimeWorker is constructed. LargePrimeWorker, which inherits from TimerWorker, is a worker class that encapsulates, inter alia, a System.Timers.Timer.
  • An instance of SmallPrimeWorker is constructed. Just like LargePrimeWorker, it inherits from TimerWorker and is a worker class with a timer.
  • The inherited RegisterWorker method is called twice - once for each of the workers that have been instantiated.

Before we move on to the concrete implementation of a worker class, let's take a quick detour and look behind the scenes at the private implementation of the RegisterWorker method used above:

/// <summary>
/// Register a worker
/// </summary>
/// <param name="worker"></param>
private void _registerWorker(TimerWorker worker)
    // Provide the worker with a handler to the function that
    //  allows it to evaluate the state of this service

    worker.getServiceStateHandler = getServiceState;

    // If this service is using a logger, set a logger for the worker too
    if (_log != null)

    // Add it to the collection

    if (_workers == null)
        _workers = new ArrayList();

    _workers.Add(new WorkerCollectionItem(worker)); // The use of this constructor will cause an
                                                    //  associated signal (ManualResetEvent) to be created
                                                    //  See WorkerCollectionItem.cs

There are two things of note that happen here:

Firstly, this line of code...

worker.getServiceStateHandler = getServiceState;

 provides each worker with a handle to a delegate function that allows the worker to query the state of the Service. This gives the worker the ability to poll for a change in the state of the Service, for example when Service is changing from a Running state to a Paused state.

Secondly, each worker is added to the _workers collection, allowing the service component class to subsequently manage signaling for each worker and to manage the disposal of each worker when the Service terminates.

We can look at this in greater detail later. For now, let's move on to an example of a derived worker class.

Here is the class definition and constructor for LargePrimeWorker:

internal class LargePrimeWorker : TimerWorker
    private Random r = new Random();
    private System.IO.StreamWriter theFile;

    /// <summary>
    /// Constructor - ensure you use an available base constructor. In this case, we specify the
    ///  constructor arguments:
    ///         -- a delayOnStart value of 30000 (approximately 30 seconds) which means nothing will happen for
    ///                 approximately 30 seconds after the service is started
    ///         -- a timerInterval value of 10000 (approximately 10 seconds)
    ///         -- a workOnElapseCount value of 6 (work will be carried out every 6th elapse of the
    ///                 underlying timer, which equates approximately to 6*10 = 60 seconds)
    /// Obviously it would be easy to create a non-default constructor for your concrete class using the
    ///  same arguments if, for example, you wanted to store these settings in your app.config (or wherever)
    /// </summary>
    internal LargePrimeWorker()
        : base (delayOnStart: 30000, timerInterval: 10000, workOnElapseCount: 6)

When inheriting from TimerWorker to construct your worker, you must use the base directive to access one of these two constructors, as there is no useful parameter-less constructor in the base class. In the example above, the second of them is used.

protected TimerWorker(double timerInterval, uint workOnElapseCount)
protected TimerWorker(double delayOnStart, double timerInterval, uint workOnElapseCount)

The only difference between the two constructors is that the former causes work to start immediately, whereas the latter implements an initial delay before work begins.

Before proceeding further, let me explain how the underlying Timer operates in line with the arguments you pass in to the base constructor. As you would expect, the Timer elapses at regular intervals consistent with the timerInterval value specified. However, the elapse does not cause work every time it occurs. Instead, a private counter is incremented on each elapse. Only when the counter reaches a value equal to the  workOnElapseCount value is the counter reset to zero and work carried out.  Nevertheless, on every elapse, the worker class makes a query to its service component class to determine if there has been a change in the state of Service.

To help explain this more clearly, let's look at how the underlying Timer will operate in the LargePrimeWorker example shown above.

The values passed in to the constructor are:

delayOnStart: 30000, timerInterval: 10000, workOnElapseCount: 6

In this case, the following will happen:

  • When the worker is constructed, the timer will sit idle for approximately 30 seconds (delayOnStart = 30000ms)
  • After a further period of approximately 10 seconds (timerInterval = 10000ms) the timer will elapse for the first time. The private counter will equal 1. On elapse, the worker will query the service for any change in its state.
  • After a further period of approximately 10 seconds (timerInterval = 10000ms) the timer will elapse for the second time. The private counter will equal 2. On elapse, the worker will query the service for any change in its state.
  • After a further period of approximately 10 seconds (timerInterval = 10000ms) the timer will elapse for the third time. The private counter will equal 3. On elapse, the worker will query the service for any change in its state.
  • After a further period of approximately 10 seconds (timerInterval = 10000ms) the timer will elapse for the fourth time. The private counter will equal 4. On elapse, the worker will query the service for any change in its state.
  • After a further period of approximately 10 seconds (timerInterval = 10000ms) the timer will elapse for the fifth time. The private counter will equal 5. On elapse, the worker will query the service for any change in its state.
  • After a further period of approximately 10 seconds (timerInterval = 10000ms) the timer will elapse for the sixth time. The private counter will equal 6, which is equal to the value of workOnElapseCount. On elapse, the worker will query the service for any change in its state, the private counter will reset to 0, and work will be carried out.
  • All of the above happens again (without the initial delay)

You can see from this example that work will be carried out approximately every 60 seconds and that the Service state will be queried approximately every 10 seconds. The jump start code encourages you to construct timers that query the Service state often. The advantage of doing so is that, provided the work you execute also completes within a reasonable period of time, your workers will remain responsive to changes in the state of their Service. This is important, because the Service Control Manager (SCM) requires that Services respond to its requests in a timely fashion. Do not underestimate the importance of getting this aspect of your Service behaviour right.

Note: You can configure workOnElapseCount with a value of 1, in which case work will be carried out on every elapse of the Timer. A key to constructing workers that are responsive to service state transitions is to ensure that the work code executed by your worker completes within a reasonable period of time; if it typically runs for more than 50-60 seconds, I recommend that you try to find a way to break it down in to smaller units. If your Service needs to get through a lot of work in a relatively short period of time, you should be looking to build workers that execute fast-running work methods more often, rather than workers that run code with lengthy execution times less often. You cannot set workOnElapseCount to a value of 0 - doing so throws an exception.

Let's look at some more code from the example implementation of the concrete worker class:

/// <summary>
/// StartWork will execute once and only once, at start up of the
///  Service
/// </summary>
/// <param name="info"></param>
protected override void StartWork(TimerWorkerInfo info)

    // Note that you get a reference to a suitable log4net logger by way of inheritance,
    //  and without any additional code, provided that your Service makes a call to
    //  DefaultLog() before registering its workers

    this.Log.InfoFormat("File '{0}' opened",
        (theFile.BaseStream as System.IO.FileStream).Name);

/// <summary>
/// Use the Work override to carry out our work
/// </summary>
/// <param name="info"></param>
protected override void Work(TimerWorkerInfo info)
    int N;
    long prime;

    // In this example, we simulate some work by calculating a random Nth prime,
    //  and writing the result out to a log file. In a real service implementation,
    //  your work would go here - polling a directory, polling a database, or
    //  whatever else it is your service worker should do at regular intervals

    Prime.GetNthRandomPrimeLarge(r, out N, out prime);

    theFile.WriteLine(string.Format("Calculated prime number with ordinal {0}", N.ToString()));
    theFile.WriteLine(string.Format("The prime is {0}", prime.ToString()));

    // The info object contains some basic information about the operation of
    //  the underlying timer

  • Override the StartWork method to carry out any initialisation work. This method will execute only once, the first time that the underlying Timer elapses to carry out work. It is a good idea to limit the amount of code you execute in the constructor of your worker. Where possible, place that initialisation code in StartWork instead.
  • Override the Work method to carry out your work.
  • The info object of type TimerWorkerInfo is made available via numerous methods, to provide some simple statistics and information about the underlying Timer. Further examine the source code to see what properties are included.
protected override void OnContinue(TimerWorkerInfo info)


protected override void OnPause(TimerWorkerInfo info)


protected override void OnStop(TimerWorkerInfo info)


protected override void OnShutdown(TimerWorkerInfo info)


The purpose of each of the above overrides ought to be self-explanatory, but I'll be brief and explicit lest there be any doubt:

  • Override OnContinue with any code that should be executed by your worker when your Service is Resuming having been Paused
  • Override OnPause with any code that should be executed by your worker when your Service is transitioning from Running to Paused
  • Override OnStop with any code that should be executed by your worker when your Service is Stopping
  • Override OnShutdown with any code that should be executed by your worker when your Service is Stopping due to a system shut down

Note: I confess that I have yet to discover any good reason for implementing different code for OnStop than for OnShutdown. Either way, your Service is stopping, right? I'm sure that other experts will be able to explain why Windows Services handle these events separately. I've never researched this for myself.

With that, we have covered the main aspects you need to be aware of to create your concrete classes using the jump start code. The above explanatory notes, together with a review of the sample code included in the article attachments, should be enough to get you well on the road to implementing your Service. The sections below offer additional insights into how it all works.

Service and worker co-ordination

In this section, I'll explain how the abstracted jump start code co-ordinates Service state transitions between a Service component class and its workers.

Co-ordinating the transition of a Service state, for example a transition from Running to Stopping, presents us with this scenario:

  • When the Service class component needs to stop, we require that its workers stop working, and in all but the most trivial of cases, the workers must be given an opportunity to stop gracefully (typically, each worker will persist some kind of data and dispose of whatever resources it is responsible for)
  • The workers will be executing code on a different thread to the Service class component, driven as they are by an underlying System.Timers.Timer
  • We therefore need to take some care to manage this communication between threads properly
  • As the jump start code abstracts both the Service class component and each of its workers, we cannot use concrete methods to meet these requirements. Doing so would undermine the benefits of abstracting this logic away into re-useable classes.

To see how this is handled, first take a look at this block from TimerServiceBase:

/// <summary>
/// Track the state of this service
/// </summary>
private ServiceState _serviceState
    = ServiceState.Running;

/// <summary>
/// A reference object to facilitate thread-locking the _serviceState
///  enum var declared immediately above
/// </summary>
private object _serviceStateLock
    = new object();

/// <summary>
/// Enum to describe the states of the Service
/// </summary>
internal enum ServiceState
    Running = 0,
    Pausing = 1,
    Paused = 2,
    Stopping = 3,
    ShuttingDown = 4,
    Stopped = 5

/// <summary>
/// Delegate definition for the function call made by workers
///  to get the state of the service
/// </summary>
/// <returns></returns>
internal delegate ServiceState getServiceStateDelegate();

/// <summary>
/// Get the state of this service. Signature matches
///  with the delegate declared immediately above
/// </summary>
/// <returns></returns>
internal ServiceState getServiceState()
    lock (_serviceStateLock)
        return _serviceState;

The ServiceState enum is used to describe the various Service states, and the current state is stored in the _serviceState variable of the same type. As we need to access and manipulate this variable across numerous threads, a reference object _serviceStateLock is created for thread locking purposes, and any code that needs to access the variable is surrounded by a lock clause.

Note: Rather than using a lock clause, we might have been able to use one of the newer Interlocked methods provided by the Framework. Personally, I prefer lock because it makes code more readable.

To address the issue of needing to abstract the code that facilitates the worker classes querying the state of the service, we create a delegate with a parameter-less signature and a ServiceState return type:

internal delegate ServiceState getServiceStateDelegate();

... and implement a function with a matching signature ...

internal ServiceState getServiceState()

... and then, when each worker is registered using the RegisterWorker method, we pass a handler to this method off to the worker:

worker.getServiceStateHandler = getServiceState;

In the TimerWorker class, on each elapse of the timer, we use a call to the getServiceStateHandler handler to query the current state of the Service, and to take whatever action may be necessary:

private void _QueryAndHandleServiceState(TimerWorkerInfo info, out bool doWork, out bool stop)
    // Query the state of the service

    TimerServiceBase.ServiceState state
        = getServiceStateHandler();

    // Handle the state appropriately...

The final piece of the puzzle is to ensure that each worker has an opportunity to stop gracefully when the Service needs to stop. This requirement is met by associating a ManualResetEvent with each worker as it is registered. You don't see this code in any of the snippets above, because it's slightly tucked away, but here it is, seeing the light of day:

worker.signalEvent = new ManualResetEvent(false);

You can get a better idea of how these ManualResetEvent signals are used by taking a look at the OnStop override of the TimerServiceBase class. (The OnPause implementation works in a very similar way as far this concept is concerned)

protected override void OnStop()
        if (_log != null)

        // Reset the signals for state transitions

        // Change the state of the service to "Stopping"
        lock (_serviceStateLock)
            _serviceState = ServiceState.Stopping;

        // Wait for all of the workers to stop. This means using a WaitAll()
        //  that runs across all of the signals in the Workers collection

        // Change the state of the service to "Stopped"
        lock (_serviceStateLock)
            _serviceState = ServiceState.Stopped;

        if (_log != null)

The above block is not entirely verbose because some code is tucked away in the private methods _resetSignals and _waitSignals, but hopefully this is sufficient for you to grasp the concept. Essentially, what happens is as follows:

  • A message is (possibly) logged
  • The ManualResetEvent associated with each worker is Reset
  • The Service state is changed to Stopping
  • The Service waits until each and every worker has detected the new state of the Service, completed its work, come to a stop, and has Set its ManualResetEvent
  • The Service changes state to Stopped and (possibly) logs a further message


It is important that you understand how the jump start code helps you with disposal of resources and where you need to pick up the slack yourself.

Firstly, observe that:

  • TimerWorker explicitly implements IDisposable and therefore any worker class you derive from it is IDisposable
  • TimerServiceBase implements IDisposable because it is derived from an IDisposable, and as such, any Service component class you derive from TimerServiceBase is also IDisposable

The good news is that TimerServiceBase incorporates code that ensures that the Dispose method of each  worker is called. (The prerequisite is that a call to RegisterWorker must have been made for the worker, but if you hadn't done that, your worker wouldn't run anyway).  So, you don't need to concern yourself with making those calls happen, and disposal of the underlying System.Timers.Timer is of course taken care of for you.

Note: In the vary rare case that it be of any consequence, and for whatever it may be worth, the disposal code in TimerServiceBase will call Dispose for each worker in the reverse order to that in which the workers were registered. That is to say, the worker that was passed to the first call of RegisterWorker will be disposed of last, and the worker that was passed to the last call of RegisterWorker will be disposed of first.

The not-so-good news, which should hardly come as any surprise, is that the usual best practise Disposing rules still apply. If, for example, you use a managed IDisposable resource in your concrete worker class, you must:

  • Use the resource with using directives so that it is automatically disposed; or
  • For any resources absent using, correctly implement the protected override dispose pattern, dispose of your resources therein, and ensure that you make a call to base.Dispose(disposing) as shown in the following block; or
  • Otherwise ensure that the resource is disposed of appropriately.
private bool disposed = false;

 // Protected implementation of Dispose pattern.
 protected override void Dispose(bool disposing)
     if (disposed)

     if (disposing)
         // Free any other managed objects here.
         if (theFile != null)

     // Free any unmanaged objects here.
     disposed = true;

     /// Because the base TimerWorker class is IDisposable, you MUST NOT forget to
     ///  base.Dispose(disposing), to ensure the underlying timer is properly disposed of

Logging with log4net

There's a wealth of resources on how to use the library so I have deliberately kept this section brief.

In your Service component class, you can call the method DefaultLog, which gets your class an ILog logger using the standard methodology:

_log = LogManager.GetLogger(this.GetType());

You can then access this in your derived class by way of the Log property.

A little more helpfully, this carries over to your workers, in as much as they each get their own type-specific _log instantiated automatically provided that you have made the call to DefaultLog in your service class before you make your calls to RegisterWorker.

Just as for the service component class, in each of your derived worker classes you can use the Log property to do your logging. (It may be sensible to check Log != null in your code, though, to avoid null-ref exceptions).

The example PrimeCalService includes a working log4net xml-based configuration that you can reference as a starting point if you are new to log4net. Just take a look at the app.config

I recommend you spend some additional time looking into the log4net documentation, because there are myriad ways to configure and use this library.

Carrying out work with longer execution times

I mentioned previously my belief that breaking your work down into units with short execution times is one of the keys to building Services that behave well.

If you have a need to run code in Work that is likely to execute over several minutes or longer, you are going to experience issues using the jump start code when your Service is stopped or paused by the SCM. The jump start code assumes that the timer of each worker will elapse regularly enough to ensure that the worker is adequately responsive to changes in the state of its Service; and the underlying timer will not elapse at all between entering and exiting Work. If this doesn't make sense to you, take a read through and understand the TimerWorker code, and I'm sure it won't take you long to see the problem.

You do, however, have the option of checking the service state during the execution of your Work. The base class provides a boolean ServiceStateRequiresStop property for exactly this purpose. If you are able to check its value often enough as your work executes and you are able to quickly break out of your work should the result be true (which means the Service state is no longer Running), that might be enough to keep your Service responsive.

Final thoughts

  • If you need to build timer-based Services for production use, you will probably want to take a close look at whether or not the jump start code includes adequate exception handling to meet your needs. You are likely to need to do your own additional work on this - the code I am providing has not been extensively tested, and I'm offering it up to the community as a conceptual model rather than anything more. In short, I'm giving you a jump-start here - not buying you a Ferrari.
  • There are probably weaknesses in this first edition of the code (I'm aware of several myself but have not yet had an opportunity to address them.) If you have ideas and feedback, please don't hesitate to let me know. I welcome any amount of constructive criticism, and will make the effort (as time allows) to incorporate improvements and update the article accordingly.
  • If you are new to Services, a few small tips that may save you some pain: (1) the first time you install with installutil, make sure that you have both added and configured an installer using the VS designer and that your project has been built with that installer in place; and that you run installutil with elevated (Run as Admin) privileges; (2) consider carefully which account to run your Service under. Local System is usually enough if your Service is accessing only local resources, but if your Service code needs to access a remote database, for example, you may need to consider other options; (3) when debugging with Attach to Process, always start Visual Studio with elevated privileges (Run As Admin).


Embedded in the Sample service attached to this article is a modified routine for the calculation of Prime numbers. The original routine was sourced here:

It is not explicitly credited by the person who posted it. If it belongs to you, please let me know and I will be happy to formally acknowledge you here.




This is the initial publication of this article.


This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


About the Author

Robert Ellis
United Kingdom United Kingdom
London (UK) software developer & IT specialist contractor

You may also be interested in...

Comments and Discussions

QuestionAs a general rule Pin
#realJSOP24-Mar-16 1:28
mve#realJSOP24-Mar-16 1:28 
AnswerRe: As a general rule Pin
Robert Ellis21-Sep-16 15:15
memberRobert Ellis21-Sep-16 15:15 
Question.NET 4.5 Pin
Jan Filtenborg Larsen14-Oct-15 1:16
memberJan Filtenborg Larsen14-Oct-15 1:16 
QuestionConsole Service Dev Options Pin
diverbw1-Dec-14 5:20
professionaldiverbw1-Dec-14 5:20 
SuggestionI like all of your work (so far)... Pin
Garth J Lancaster29-Oct-14 18:42
professionalGarth J Lancaster29-Oct-14 18:42 
GeneralRe: I like all of your work (so far)... Pin
Berry van Olphen30-Oct-14 2:58
professionalBerry van Olphen30-Oct-14 2:58 
GeneralRe: I like all of your work (so far)... Pin
Garth J Lancaster30-Oct-14 13:01
professionalGarth J Lancaster30-Oct-14 13:01 
GeneralRe: I like all of your work (so far)... Pin
alekcarlsen30-Oct-14 21:51
memberalekcarlsen30-Oct-14 21:51 
QuestionWhere is the code? Pin
Dewey29-Oct-14 13:12
memberDewey29-Oct-14 13:12 
AnswerRe: Where is the code? Pin
Robert Ellis29-Oct-14 15:50
memberRobert Ellis29-Oct-14 15:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web03 | 2.8.190306.1 | Last Updated 26 Nov 2014
Article Copyright 2014 by Robert Ellis
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid