.NET Instrumentation Workshop
Home About Workshops Articles Writing Talks Books Contact

5. Asserts

A trace message can be regarded as a polite progress report; an assert is a loud warning. Traces can give you information that things are going well, or intermediate results so that if things do fail then you can backtrack and find out why; an assert indicates that an important variable is wrong and that the programme has failed. Trace and asserts are clear very different and you should use them differently.

Asserts must only be active in debug code because when they are triggered they indicate that there is a bug, that you guessed that a bug was possible, but all that you did about it was to insert the assert code. It makes your code look very unprofessional if an assert appears in a released product. Furthermore, the assert could generate a modal dialog that blocks the thread.

Asserts are extremely useful in debug code, but they should never be used in released code.

5.1 Windows Error Dialogs

Before explaining asserts I first need to explain some of the error dialogs that Windows can show. Windows allows you to define a debugger by providing values in the registry. The system debugger is specified by providing values in the AeDebug key:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug

The values that you'll see in this key are shown in the next table.

Value Description
Auto When this is set to 1 the debugger is attached automatically to the faulting process; when it is set to 0 a dialog is shown asking if the debugger should attach.
Debugger The path and command line to attach the debugger to the process. The parameters are the process ID and the handle of an event that you use to indicate that the process has attached.
PreVisualStudio7Debugger If Visual Studio is installed, this will have the value that Debugger had before it was changed to invoke the VS debugger
UserDebuggerHotKey The virtual key code of the hot key used to set a breakpoint when a process is running under a debugger

When the system launches the debugger it will use the launch string as a C-like format string, so you can use format specifiers in the string to get the parameters from the system. For example, when you install Visual Studio the debugger is started with vsjitdebugger.exe -p %ld -e %ld, here the -p switch is passed an integer that is the process ID of the process to attach to, the -e switch is passed the handle of an event which the debugger will set once it has attached to the process.

When an unmanaged application throws an exception the system will use the values in the AeDebug key to determine which debugger to invoke and how. However, Windows has to determine if the debugger should be launched. The System control panel applet (Control Panel, System) Advanced tab has an Error Reporting button. This leads to a dialog that will allow you to determine if error reporting will be used:

If error reporting is disabled (and the But notify me when critical errors occur check box is clear) then you will not see an error dialog when an exception is uncaught. So for this section you must make sure that error reporting is enabled as shown in the screenshot. In this case Windows will look in the following key:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PCHealth\ErrorReporting\DW\Installed

for a value DW0200. This is a path to the error reporting tool, and usually it is called dw20.exe under the Microsoft's shared files in Program Files.

A managed application will run under the managed environment, which will handle managed exceptions. The .NET runtime uses the following key to get values to determine how an uncaught exception is handled:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework

The relevant values in this key are:

Value Description
DbgJITDebugLaunchSetting This setting is used to indicate how a managed exception is handled, a value of 0 means that the unhandled exception dialog is shown; a value of 1 means that the stack trace is printed and the process is terminated; a value of 2 means that the managed debugger is invoked
DbgManagedDebugger The command to start the managed debugger

The DbgManagedDebugger value will be passed parameters, and again the string in this value is a C-like format string. There are four possible values, in this order: the process ID, the ID of the application domain, a string that is the exception type and a value that is an event handle which is set by the debugger when the process is aborted.

Try this out. Create a file (ex.cs) with this code:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

class App
{
   [DllImport("kernel32")]
   static extern void RaiseException(
      uint exceptionCode, uint exceptionFlags, uint numArgs, IntPtr args);

   static void Main(string[] args)
   {
      if (args.Length == 0) return;
      switch(args[0][0])
      {
      case 'e':
         throw new Exception("Problem");
      case 'd':
         Debugger.Break();
         break;
      case 's':
         RaiseException(0, 0, 0, IntPtr.Zero);
         break;
      default:
         break;
      }
      Console.WriteLine("Completed");
   }
}

You have three options. If you pass e as the parameter then a .NET exception is thrown. If you pass d then the user will be given the option to attach the debugger. The final option is s which will raise an unmanaged structured exception (SEH).

Compile this code. Now open regedit, navigate to HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework. and write down the value of DbgJITDebugLaunchSetting; then navigate to the Windows NT\CurrentVersion\AeDebug key and write down the value of the Auto value so that you can restore their values when you are finished.

Now try all combinations possible for DbgJITDebugLaunchSetting (0, 1, 2) and try each parameter for ex (e, d, s).

These are the results for a value of 0 for Auto.

DbgJITDebugLaunchSetting 0
1
2
.NET Exception System error dialog (1), followed by Exception stack dump Stack dump Attach debugger dialog: attach to debugger or stack dump
Attach Debugger System error dialog, followed by continued execution (2) Execution continues Attach debugger dialog: attach to debugger or continue execution
SEH Exception System error dialog (1) followed by SEHException stack dump SEHException stack dump, followed by system error dialog (3) SEHException stack dump, followed by system error dialog (3)

Interestingly, if you set Auto to 1 then you get similar results except that for an SEH exception and a DbgJITDebugLaunchSetting setting of 1 or 2 you get the 'attach debugger' dialog (that is, vsjitdebugger). This is as you expect, Auto is used to determine if the debugger in Debugger is called and this will only occur for unhandled unmanaged exceptions.

The system error dialog has three types, as indicated in the table above. The first two are essentially the same, the difference is the caption. The first (1) is for an uncaught .NET exception:

Notice the button called Debug. If you click on this button then the debugger will be invoked and attached to the faulting process. The second (2) has a different message indicating that a break point was hit:

The final dialog (3) is for an uncaught SEH exception:

This includes an apology and a hyperlink, and significantly, it does not follow the same Windows theme as the other dialogs, indicating to me that it is being generated by lower level code. If you click on the hyperlink you'll get a brief description of the error condition:

This, too, has a hyperlink to more information:

Finally, the 'attach debugger' dialog (vsjitdebugger) looks like this:

Note that if you have just the runtime redistributable installed, but not Visual Studio, then there are no debugger values under the .NETFramework registry key and the AeDebug key has Auto set to 1 and Debugger set to use drwtsn32. In this case you will get the dialogs (1), (2) and (3), but you will not have a Debug button.

Also, note that you might find that in the cases when I indicate that the system error dialog (1) is shown you will see a dialog with more information like the following:

This is the same as the dialog shown above, except that you do not have the option to get more information. The additional information is shown if the AllOrNone value is set to 1 in:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PCHealth\ErrorReporting

At this point, return the registry back to the settings you had before.

5.2 Fail

The Debug and Trace classes have an overloaded method called Fail which will only be called if the DEBUG or TRACE symbol is defined, respectively. These will call the Fail method on every trace listener in the Listeners collection and, interestingly, if AutoFlush is true the trace listener's Flush method will be called. Of the two overloads, one takes a string, and the other takes two strings. The single string is the failure message, the second string is described as being the detailed message.

The DefaultTraceListener class implements Fail by writing a stack trace and the message(s) to the output debug string, and if LogFileName has a value, this data is written to the log file. In addition, there is an AssertUiEnabled property and if this is set then a modal dialog is shown on screen with this information. The values of LogFileName and AssertUiEnabled can be set through the configuration file. Curiously they have their own section, <assert>, even though they only refer to instances of the DefaultTraceListener class.

To try this out, create a file (app.cs) and add the following code:

#define DEBUG
using System;
using System.Diagnostics;

class App
{
   static void Main()
   {
      Debug.Fail("Something bad has happened",
         "If I knew what the problem was I would tell you");
   }
}

Compile this as release mode, that is, do not use the /debug switch. Run dbgview and then run the process.

The most immediate thing you see is a modal dialog:

This is the standard Win32 MessageBox, this API has limited options, so the designers decided to chose the dialog with the Abort, Retry and Ignore buttons. Because these names do no correspond to the actions that are offered, the caption of the dialog has a handy key: Abort means that the application will stop immediately; Retry will mean that the debugger will be started and attached to the process and Ignore means that the problem will be ignored  and the process will continue executing as before. You'll try out the buttons in the next section so for now just click on the Abort button whenever you see this dialog.

So why didn't the designers of the framework use Windows Forms to create a dialog? Well the obvious answer is that this means that any program that uses Fail will have a dependence upon the Windows Forms library. However, it is easy to call the Win32 API to create a dialog, I am surprised that Microsoft didn't do this extra work.

If you take a look at dbgview you will see something like this:

Now compile the process but this time compile a debug build (/debug). Run the process and you'll see the same assert dialog, but this time you'll get a bit more information:

The dialog indicates where in the source file where the Fail was called. The reason is that a stack dump is performed (in this case the Fail was called in the entry point) using the StackTrace class and if debugger symbols are available this class will use the symbols to get source file information.

Now create a configuration file app.exe.config.

<configuration>
   <system.diagnostics>
      <assert assertuienabled="false"/>
   </system.diagnostics>
</configuration>

Run the application again, without compiling. This time you'll see that the dialog will not be shown, but the message will be shown in dbgview.

As another test, add the following attribute:

<assert assertuienabled="false" logfilename="app.log"/>

Run the application again. This time you'll see that the same information that is shown by dbgview will appear in the log file.

If you code is a service then it will not have a user interface - if your service process has a UI you have implemented it wrong and you should start again, the UI for a service must be a separate process. A service runs in a different window station to the interactive user, which means that if a dialog is shown by a service the user will not see it. More worryingly, a modal dialog will block the thread and since the dialog will not be visible, and there will not be anyone to click on any of its buttons, it means that the thread will be permanently blocked. This is why assertuienabled is so important because it ensures that Fails will still be reported but the thread will not be blocked. Personally I would have preferred the dialog to be an opt-in (that is, disabled by default) rather than the current situation where it is opt-out (it is enabled by default).

Note that Trace.Fail is marked with the [Conditional("TRACE")] attribute, that is, if you use the Visual Studio projects and you include Trace.Fail, this method will be called in release mode. Again, learn this lesson: do not define TRACE for a release build.

Neither TextWriterTraceListener, nor EventLogTraceListener, nor FileLogTraceListener implements Fail, so if you use these trace listeners withy code that calls Debug.Fail or Trace.Fail then you'll get the implantation provided by TraceListener which will create a message in the following form:

TraceListenerFail <message> <detailed message>

and call the virtual method WriteLine. So, for TextWriterTraceListener (or FileLogTraceListener) the string is written to the log file, and for EventLogTraceListener the string is written to the event log.

5.3 Assert

The problem with Fail is that it is unequivocal - it is a failure. An assert is different because it performs a check. There are three overloads to Assert, in all cases the first parameter is a Boolean, the other two parameters are a message and a detailed message. The Assert methods on Debug and Trace just delegate the work to the TraceInternal class, which merely checks the condition and on failure calls the appropriate Fail. For example:

public static void Assert(bool condition, string message, string detailMessage)
{
   if (!condition)
   {
      TraceInternal.Fail(message, detailMessage);
   }
}

It is that simple. So let's try it out. Create a file (app.cs) and add the following:

#define DEBUG
using System;
using System.Diagnostics;

class App
{
   static void Main(string[] args)
   {
      Debug.Assert(args.Length > 0, "You must provide arguments");
      Console.WriteLine("you passed {0} as the first argument", args[0]);
   }
}

Compile and run this application: first, provide one argument and confirm that the application runs as expected; next run it without a parameter to prove that you get the assert dialog with this message:

You must provide arguments

at App.Main(string[] args);

Notice that unlike the C++ _ASSERTE macro the message does not contain the expression that failed. That is entirely your responsibility and perhaps you can use the second string parameter of Assert to provide this information. If you click on Abort at this point the process will stop. Don't do that, instead click on Ignore.

Unhandled Exception: System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at App.Main(String[] args)

What's happened here is that you have not provided any parameters which has triggered the assert. When you click on Ignore the code continues after the call to Assert and since the following line attempts to access the first parameter you get an exception. This is the whole point of the assert: you know that if the arg array is empty then the program will not work. The assert flags this up to the tester, essentially the assert is an immediate indication that there is a condition that will cause the program to fail. This is of no use at all to your users. It is of immense use to your testers. Look at the exception stack dump, what does it say? Compare it to the text in the assert message. Which is the most useful? Assert messages are more descriptive for the precise reason that you are testing for a specific condition and you can indicate when that particular condition fails. Exception messages are usually more generic, which makes them less useful.

However, even though assert messages are more descriptive than exception messages you still might not know why a condition failed. Run the process again with no parameters. Again you see the assert dialog saying that you need to provide parameters. This time click on the Retry button. Assuming that you have Visual Studio 2005 and assuming DbgJITDebugLaunchSetting is set to allow debugger launch (2) the runtime will launch he debugger given in the DbgManagedDebugger value. This is vsjitdebugger.exe which gives you the option of launching the actual debugger. You will get the option of launching the Visual Studio debugger or the CLR debugger (a cut down version of the Visual Studio debugger provided with the .NET SDK). vsjitdebugger gives you two check boxes: the first, Set the currently selecteddebugger as the default will automatically launch the selected debugger the next time a crash occurs; the second, Manually choose the debugging engines, refers to the fact that different code requires different debuggers. If this check box is not selected then the debugger tries to detect if the code is native, managed or script, if you check the check box then you get to choose.

The dialog has two buttons, Yes and No. If you chose No then the exception will be returned back to the runtime, which will perform a stack dump of the exception. If you choose Yes then the debugger will be started and it will be attached to the faulting process.

Visual Studio implements a COM object to allow debugging and vsjitdebugger creates an instance of this object (and hence, if you choose an new instance of VS then creating the object will create an instance of VS) and tells the COM object to attach to the process. At this point you'll see the x86 of the JITted code. The debugger will be stopped within Debugger.Launch and so you'll have to move up the call stack to the method that called Debug.Assert. For example:

If you have symbols for the process (compiled with /debug) then the disassembly window will show source code and x86 code, and the debugger will open the source file, so that you can single step through the source code.

If you want to perform the same action yourself, that is attach the debugger to a running application, you can invoke the JIT debugger by passing the process ID (from task manager, for example) through the /p switch to vsjitdebugger.

5.4 What Should You Assert?

The first thing to be aware of, is that Assert is conditional, which means that it will not be compiled if the appropriate symbol is not defined. What is not so obvious is that you should not call any code as a parameter to a conditional method that might change state. This is because if the conditional method is not called then neither are any properties or methods that you pass as parameters. For example, create a file (app.cs) and add this code:

using System;
using System.Diagnostics;

class App
{
   static void Main(string[] args)
   {
      Data data = new Data(args.Length > 0 ? args[0] : null);
      data.DoSomething();
   }
}

class Data
{
   string str;
   public Data(string s)
   {
      str = s;
   }
   public void DoSomething()
   {
      Debug.Assert(IsDataCorrect, "Internal state inconsistent");
      Console.WriteLine("Done something with {0}", str);
   }
   public bool IsDataCorrect
   {
      get
      {
         if (str == null) return false;
         if (str.Length == 0) return false;
         return true;
      }
   }
}

Here, the number of parameters is tested and if there are parameters then the first one is passed to the Data constructor, otherwise null is passed. The Data class has a property called IsDataCorrect that determines if the internal state of the method is correct. If it is not correct it will return false. This property is called in the Assert so that if the internal state of the object is not correct then the developer will be informed during debugging.

Compile it defining DEBUG:

csc /d:DEBUG app.cs

Now run this code with a single parameter, you'll see that it works as expected. Run it without a parameter and you'll find that an assert dialog will be shown. Click on Abort. Now imagine that the developer of Data wants to takes steps to ensure that instances are correct. He might decide to do this:

public bool IsDataCorrect
{
   get
   {
      if (str == null)
      {
         Console.Write("Please give a value: ");
         str = Console.ReadLine();
      }
      if (str.Length == 0) return false;
      return true;
   }
}

Compile this code as before. Run it with a parameter and then without a parameter; when you are asked for a value type something and press Enter. In both cases you'll find that the program will work correctly, it will give a message that it has done something with the data that you provided. Here's the result from the last test:

C:\Instrumentation\5.4>app
give a value: test
Done something with test

Now run it again without a parameter, and when asked for a value just press Enter. You'll see that an assert dialog will appear, as expected. (I find that in this case the assert dialog will not be topmost, however, you will see that it appears on Windows' task bar.) Abort the program. The program appears to work as expected. However, try compiling for release mode, that is, do not define DEBUG on the command line. Run the process without a parameter. What do you see? Here's the results I get:

C:\Instrumentation\5.4>app
Done something with

Notice that you are not prompted to enter a value. The reason is that the entire line that calls the Assert is not compiled, including the call to the IsDataCorrect property.

The lesson to learn from this example is that whatever you call (method or property) as a parameter to a conditional method should not change the state of any objects in your application. If they do have such side effects it means that your code will run differently in release mode than it does in debug mode.

Back to the topic of this section: what should you assert? Let's start with a few rules about assertions and then I'll go into details afterwards

  • asserts are used to find implementation errors
  • asserts are not a replacement for defensive programming
  • use asserts to make bugs reveal themselves
  • beware of side effects
  • use asserts to check consistency
  • do not use asserts to validate user input

Defensive programming ensures that errors are handled accordingly, asserts go hand-in-hand with defensive programming, but is not a replacement. The reason is that asserts are a debugging tool and should be used to get bugs to reveal themselves, and not to handle bugs. If a method takes a string parameter and requires that the parameter is not null then an assert will reveal a null parameter during debugging and allow you to pay more attention to where that parameter came from and hence make sure that a null parameter cannot be passed in future. At runtime, for a release build, the asserts will not be active (and should not be active) and so this check is not performed. Such a null parameter still has to be handled in a release build, or else it could lead to invalid results or to an exception.

Asserts are used to detect a condition that should always be true if the program is running correctly. Of course, the reverse is not the case, if asserted conditions are true it does not necessarily mean that the program is running correctly. It is possible for asserts to detect if the program is not running correctly, but this depends on how many asserts you have. It makes little sense to assert everything, because that would make the code unreadable. Further, you should not assert conditions that you think could fail because of a possible bug. If you think that there is a possible bug then you should take steps to fix the bug, in which case the assert is unnecessary. Thus, a balance must be struck to perform enough tests, but not to perform too many.

There are essentially two levels of conditions that you should check, which I will call major conditions and minor conditions. The major conditions are data consistency (class invariants and loop invariants) and validating method parameters. Class invariants are conditions that must be true for an instance of the class to be valid, similarly, loop invariants are conditions that must be true before the loop starts and true at the bottom of the loop code (ie before the loop terminates, or before the next iteration).

For example, a date class will hold data for the day, month and year. The month should be between 1 and 12 and the day should be between 1 and 31. For example:

class Date
{
   int day, month, year;
   // other code
   [Conditional("DEBUG")]
   private void AssertValid()
   {
      Debug.Assert(day >= 1 && day <= 31, "Day is out of range 1 <= day <= 32");
      Debug.Assert(month >= 1 && month <= 12, "Month is out of range 1 <= month <= 12");
   }
}

In this case the AssertValid method checks to see if the instance is valid, and performs two asserts. The method is marked as conditional because it will be empty if compiled in release mode. (The astute amongst you will notice that these conditions are too simplistic, because not every month has 31 days, and February has 28 or 29 depending on whether the year is a leap year. For simplicity's sake, I will not add those checks.)

This method checks that the object is valid, it does not necessarily check that an operation is valid. So, if a Date instance is initialized to December the 20th and you call a member function to increment by twelve days the assertion will show a bug if the operation results in a date of December the 32nd. The assertion will not detect if the operation of adding twelve days resulted in January 2nd because that is a valid date.

Class invariants are so named because when an operation is carried out on an instance the conditions will still remain true. Such operations are necessarily public operations (methods or property accesses). Private and protected members can only be called by code internal to the class or derived classes, and ultimately they will be called by public members. However, they will be called within those public members when the object will be in a state of change and hence invariants cannot be true. For example:

public void Increment(int days)
{
   AssertValid();
   day += days;
   int daysInMonth = DaysInMonth();
   while (days > daysInMonth)
   {
      days -= daysInMonth;
      month++;
      if (month > 12) month = 1;
      daysInMonth = DaysInMonth();
   }
   AssertValid();
}

private static int[] days = new int[]{31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 30};

private int DaysInMonth()
{
   Debug.Assert(month >= 1 && month <= 12, "month is out of range");
   if (month == 2) return DaysInFebThisYear();
   return days[month - 1];
}

The public method Increment first checks that the object is valid and then performs the increment routine by simply adding the required number of days to the day member and then if this is an invalid value it decrements the number of days in this month and then moves to the next month. The object will be invalid during this routine, so the private methods DaysInMonth and DaysInFebThisYear must not call AssertValid. However, the DaysInMonth method assumes that the month value is between 1 and 12, and so an assert is performed on this condition. This shows that although the object as a whole is invalid, parts of it should be valid for specific routines to work correctly.

Loop invariants are not quite as obvious as class invariants. A loop invariant is a condition that is true before the loop starts and is true when an iteration of the loop has completed (but may not be true during the iteration). The loop test condition is not part of the loop invariants, the test condition simply determines if another loop should be performed. For example, a routine that determines the smallest value in an array will have a condition of position < data.Length as the loop check (assuming position is the current index and data is the array), but the loop invariants will be the following (assuming indexOfSmallest is the index of the smallest element found so far):

  1. indexOfSmallest is a value between 0 and position
  2. for every index between 0 and position the value in the array is greater or equal to the value at indexOfSmallest

Thus:

int IndexOfSmallestElement(int[] data)
{
   int position = 0;
   int indexOfSmallest = 0;
   for (; position < data.Length; position++)
   {
      if (data[position] < data[indexOfSmallest)
         indexOfSmallest = position;
   }
   return indexOfSmallest;
}

In this simple example it is not necessary to perform the checks on the loop invariants.

The other thing that you can assert are parameters, however, you should be wary of asserting parameters of public methods. Asserting parameters of private methods is fine, because an invalid parameter will be caused by a bug in the current class. The parameters of a public method will be passed by another class and as such do not represent a bug in the current class, it represents a bug in the calling class. Thus, you should throw an ArgumentException as part of your defensive programming to indicate that the calling code is bad. However, you may also choose to fail an assert as well to aid your testing:

public Data(int day, int month, int year)
{
   if (month < 1 || month > 12)
   {
      Debug.Fail("month is out of range");
      throw new ArgumentException("month is out of range");
   }
   this.Month = month;
   if (day < 1 || day > DaysInMonth())
   {
      Debug.Fail("day is out of range");
      throw new ArgumentException("day is out of range");
   }
   this.day = day;
   this.year = year;
}

Note that since the error handling in this code already performs a check there is no point in calling Assert, all you want to do is to inform the user, and so Fail is called. The defensive programming is far more important than the assert, so make sure that you write the error handling code before you write any asserting code.

That covers the more important assertions, now I want to cover assertions that I regard as being less important. For example, it can be useful to assert return values, but in general your error handling should ensure that 'correct' and expected values are returned. It is more useful to assert loop counters, particularly when the the limit of the counter is determined programmatically. This allows you to pick up on situations like overflows which could cause the loop to be performed far more times than you intend. Another thing to assert is impossible values in a switch.

Finally, it is worth pointing out that although you can assert conditions in services you must not perform UI asserts. A service runs under a different security content to the interactive user and hence any 'user interface' used by the service will be on a different desktop to the user, a desktop that is not visible to the user. (If the service process has user interface that is visible to the interactive user then it should be through a process running in the user's security context. The Allow service to interact with desktop setting is evil, do not use it. If you are tempted to use it, you open a security hole and deserve all that happens to your process.) The user will not see any dialogs generated by a service, moreover, a modal dialog will block the thread with no possibility of anyone dismissing it. You could depend on the configuration file setting assertuienabled to be false, but this means that someone may set it to true. Instead, you should disable this ability programmatically with code like this in the service entry point:

foreach(TraceListener tl in Debug.Listeners)
{
   DefaultTraceListener dtl = tl as DefaultTraceListener;
   if (dtl != null) dtl.AssertUiEnabled = false;
}

This ensures that if the user decides to add a DefaultTraceListener to the Listeners collection then the object is configured correctly. This is such an obvious action that I am surprised that the framework library does not do this as part of the ServiceBase class.

I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Page Six

This page is (c) 2007 Richard Grimes, all rights reserved