Beyond exception handling

This article was originally published in VSJ, which is now part of Developer Fusion.
Exceptions are the preferred way to handle errors in .NET, and this is a good thing. We programmers just can’t be trusted to test return codes or call GetLastError() in all the right places. When someone passes you an invalid null parameter the thing to do is to throw an ArgumentNullException which forces them to do something about the problem. Unfortunately, deciding what to do when the called code throws an exception is less obvious. Unless you have thought out your error processing strategy early in the development cycle, your options are usually limited to presenting an obscure message to the user and then terminating, or simply letting the operating system do the job for you.

This article describes an approach to processing errors initially developed for an open-source Windows Forms application, WinFITRunner. However, to simplify matters the article’s source-code contains just three simple Visual Studio projects; AppToolBox – a library for my classes, AppToolBoxNUnit – unit tests for the library, and TestWinApp – an application serving as a test-bed which supports both English and German speaking users. The code is written in C#, but you shouldn’t have too much difficulty translating it into any other .NET language or applying the concepts to other types of projects. I’ve made few assumptions about the reader’s technical background, beyond having an understanding about exceptions as well as some experience of building Forms applications and class libraries using Visual Studio .NET.

Delivery of Value

A message like “file system error has occurred” has little value, particularly when it is followed by an abnormal program termination. I wanted to replace such messages with ones that deliver high value to both the customer (user) and the developer, so started thinking about the value of an error message from each of these viewpoints.

The customer’s idea of a valuable error message is one that appears immediately after some failing action was instigated, explaining the cause of the problem and what can be done there and then to overcome it. For example, “unable to save your work to the specified file – try saving it to a different drive or folder”. The message must be articulated in the user’s language, which means it must be appropriately localised and be expressed in terms likely to be understood.

The developer’s idea of a valuable error message is completely different. In this case, the message needs to persist for a considerable time after the failing action was instigated and explains how the error was generated so that at some later date a work-around or fix can be created. For example, “FileStuff.cs line 10: user Sarah does not have permission to write to directory c:\abc\”. The developer wants the message to help him recreate the problem by identifying the code being executed and the state of the system when the error was recorded.

Thinking about error messages in this way, it became clear that the same message could never deliver acceptable value to both the customer and the developer. Therefore I decided that each error detected would result in two different messages; a transient message to help the user respond to the problem, and a persistent message to help the developer understand how the problem had arisen during subsequent analysis.

Developer Messages

Messages are generated by throwing an exception object derived from the .NET framework class ApplicationException. The following listing shows the implementation of my UniException class:
public class UniException : ApplicationException
{
	public enum Category { System, Resource, Data, User, Program }
	public enum Reset { Abort, Reinit, TermApp }

	private string m_MsgId;
	private Reset m_Reset;
	private Category m_Category;
	private string m_Fmt;
	private object[] m_Args;

	public UniException( string aMsgId, Reset aResetAction, Category aCategory,
		string Fmt, params object[] args): base(String.Format(Fmt, args))
	{
		//Init() simply assigns the parameters to variables
		Init(aMsgId, aResetAction, aCategory, Fmt, args);
	}
	//full implementation given in UniException.cs
A typical error statement would be:
throw new UniException(“MXC-007”,
	UniException.Reset.Abort,
	UniException.Category.Resource,
	e,
	“not enough memory:{0}”,
	RamSize);
This statement combined with the information contained in the program log is to the developer what the error message dialog is to the User. Let’s now look at the purpose of each parameter in this error statement:

Inner Parameter – e
Inner exceptions allow you to capture an exception and then wrap it inside a further exception so as to make it more meaningful to your program. This is useful when the error statement is inside a catch block.

MsgID Parameter – MXC-007
The MsgID identifies the message generated and being a string it’s easy to locate its source in your codebase using ‘Edit | Find in Files’.

Reset Parameter – Reset.Abort
After reporting an error to the user you need to restore the program back to an operational state. In other words you need to perform some form of reset. The Reset parameter allows the programmer to determine the level of reset according to the location of the error statement in the code – see Abort, Reset or Terminate.

Category Parameter – Category.Resource
Both developers and customers benefit from having a way to categorize errors. However, the more complex the categorisation system the less useful it becomes so I defined just:

  • System: any service or facility that is external to your program – access denied, network not available, program not found, etc.
  • Resource: anything directly provided to your program by the operating system – insufficient memory or disk space, etc.
  • Data: information used by your program – time (date) expiry, out of valid range, data not found, etc.
  • User: actions performed by the user – bad data, no such file, etc.
  • Program: anything not otherwise categorised
Format String and Variable Parameter List – “memory = {0}”, RamSize
The format string is the main component of the developer message and explains the problem in technical language within the context of the code, thus helping to document the program. It is augmented by an optional variable parameter list that allows you to record information about the state of various data and objects associated with the error when the statement was executed.

Customer Messages

Users expect a program to work, and when it doesn’t they want to know why an error has occurred, what they can do to overcome the problem, and get instructions on what to do next. Furthermore, this information needs to be supplied in a consistent way so users can have confidence that any problem they might encounter while using your program will result in the appearance of a familiar dialog box that will help them resolve the matter.

Figure 1: The Customer Error Message Dialog
Figure 1: The Customer Error Message Dialog

I implemented these requirements by creating a dialog box (UniMsgDlg – see Figure 1) containing a number of controls that could be filled with items stored as resources corresponding to the unique message identifier – MsgId. The class that represents this error information called is UniMsg:

[XmlRoot(“UniMsg”)]
public class UniMsg
{
	[XmlElement(“MsgId”)]
	public string MsgId
	{
		get { return m_MsgId; }
		set { m_MsgId = value; }
	}
	[XmlElement(“Cause”)]
	public string Cause
	{
		get { return m_Cause; }
		set { m_Cause = value; }
	}
	//full implementation given in UniMsg.cs
The dialog’s controls are initialised in its constructor by assigning the appropriate properties of UniMsg to them and since most of these properties are strings this is trivial. The only real coding task is displaying the dialog box buttons according to the value of the Reset parameter:
public UniMsgDlg(UniMsg aMsg, UniException.Reset
	aReset, UniException.Category aCategory, string aFmt,
	params object[] aArgs)
{
	InitializeComponent();
	if ( imageListCat.Images.Count > (int)aCategory )
		picBoxMsgCat.Image =
			imageListCat.Images[(int)aCategory];

	labelMsgId.Text = aMsg.MsgId;
	labelCause.Text = aMsg.Cause;
	textBoxTip.Text = aMsg.Tip;
	labelWhatToDo.Text = aMsg.WhatToDo;

	buttonOK.Visible = (aReset ==
		UniException.Reset.Abort)? true : false;
	buttonYes.Visible = (aReset ==
		UniException.Reset.Abort)? false : true;
	buttonNo.Visible = (aReset ==
		UniException.Reset.Abort)? false : true;
}
//full implementation given in UniMsgDlg.cs
Normally the reset operation is simply to abort the action and so a single ‘OK’ button needs to be displayed. However, higher levels of reset may result in a significant inconvenience to the User and for this reason I allow the option of ignoring the error and continuing to permit the saving of files and so forth. This requires the display of ‘Yes’ and ‘No’ buttons to put the User in control of deciding what is more important: continuing with some unknown error or losing an hour’s work.

In order for the dialog box to display the information associated with a given MsgID the the UniMsg object needs to be obtained from my collection type UniMsgList. Adding and retrieving the UniMsg objects from this collection was achieved by wrapping the ‘add’ and ‘iteration’ behaviour of an ArrayList with the Add() and GetMsg() methods of UniMsgList.

I used the XML serialisation facilities provided by the .NET framework in order to save and load my list of messages to and from persistent storage. This simply required me to provide get and set property methods for the items requiring serialisation, add some custom attributes, and write a few lines of code to serialise and deserialise the list:

public static UniMsgList LoadList(object ObjectInAssembly,
	string MsgResourceFile, CultureInfo culture)
{
	UniMsgList rc = null;

	try
	{
		Stream stream = null;
		Assembly asm = ObjectInAssembly.GetType().Assembly;
		if ( culture != null )
			asm = asm.GetSatelliteAssembly(culture);

		if ((stream = asm.GetManifestResourceStream(MsgResourceFile)) == null )
			throw new System.InvalidOperationException(“Resources not found”);

		XmlSerializer serializer = new XmlSerializer(typeof(UniMsgList));
		rc = serializer.Deserialize(stream) as UniMsgList;
	}
	catch(Exception e)
	{
		Program.FatalErrorAbend(iMsgList.LoadList()”, e.Message);
	}
	return rc;
}
//full implementation given in UniMsgList.cs
In order to get the code to pass my unit tests, I initially just saved and restored the list from a discrete file. The XML message file was created by running a small unit test that added some messages to UniMsgList and then invoked SaveList() to write them into a physical file on my hard disk:
<?xml version=”1.0”?>
<UniMsgList xmlns:xsd= ...>
	<MessageList>
		<Message>
			<MsgId>TST-001</MsgId>
			<Cause>
				Your PC has run out of
				memory
			</Cause>
			<Tip>
				Shutdown any programs that
				you are not using and...
			</Tip>
			<WhatToDo>
				Press OK to abort
				new image creation
			</WhatToDo>
		</Message>
	</MessageList>
</UniMsgList>
To take advantage of .NET framework’s support for localization, the XML message file needed to be compiled as a resource, so I added the file created by my unit test to TestWinApp using ‘Project | Add Existing Item’. I then changed the file’s property to ensure it would be built as a resource by selecting it in the Solution View, clicking the properties tab, and altering its build action to “embedded resource”. After rebuilding the project, I used the Intermediate Langauge Disassembler supplied with the .NET toolkit (ildasm.exe) to confirm that my messages had actually been embedded in the executable. The tool allowed me to open TestWinApp.exe (File | Open) and reveal the contents of its manifest, where my message file was listed as “.mresource public TestWinApp.TestWinAppMsg.xml”.

I altered LoadList() so it would load the message from a resource embedded in the application’s assembly instead of from a discrete file. I did this by creating an Assembly object corresponding to my executable and then using its GetManifestResourceStream() method to obtain a stream for the resource that could be deserialised into a UniMsgList object:

public static UniMsgList LoadList(object ObjectInAssembly,
	string MsgResourceFile, CultureInfo culture)
{
	UniMsgList rc = null;

	try
	{
		Stream stream = null;
		Assembly asm = ObjectInAssembly.GetType().Assembly;
		if ( culture != null )
			asm = asm.GetSatelliteAssembly(culture);

		if ((stream = asm.GetManifestResourceStream(MsgResourceFile)) == null )
			throw new System.InvalidOperationException(“Resources not found”);

		XmlSerializer serializer = new XmlSerializer(typeof(UniMsgList));
		rc = serializer.Deserialize(stream) as UniMsgList;
	}
	catch(Exception e)
	{
		Program.FatalErrorAbend(iMsgList.LoadList()”, e.Message);
	}
	return rc;
}
//full implementation given in UniMsgList.cs
The original LoadList() parameter for the name of the file was renamed to express the fact that it was now the name of the resource as it appears in the assembly’s manifest. I also added a second parameter to allow the specification of the assembly containing the embedded messages, as typically this would be different to the assembly containing UniMsgList and the other classes in my AppToolBox library. The unit tests that I had written to validate LoadList() whilst it was operating on a discrete file served to confirm that these changes had no adverse effect on the behaviour of UniMsgList.

I was now able to define an error statement in the form of a UniException and display the corresponding customer message in a UniMsgDlg dialog box using resources embedded in the application. My next task was to find a way for my WinForms application to handle a UniException so that the customer message would be displayed and the programmer’s message written to the application’s log.

Exception Handling in WinForms

When an exception is thrown in .NET, the Common Language Runtime (CLR) looks for an exception handler for the method that threw the exception, and if none is found it will walk the stack until it finds one. If the CLR reaches the final method in the stack, main(), and still can’t find a handler, then the exception will be declared unhandled and the application terminated.

In a console application you can handle any exceptions thrown by the program that are not otherwise caught by putting a try-catch block in the main method (or by providing an event handler for unhandled exceptions in the current application domain). However, a WinForms application provides its own unhandled exception event whose handler is invoked before the stack is unwound to main(). The default handler displays an error message and allows the user to decide whether to terminate the application or to continue, an option that simply would not be available if the stack had been unwound to main(). Of course, if your application causes some catastrophic failure in the framework classes then the stack may still be unwound to main() making program termination inevitable, but this sort of event should have been eliminated long before your code moves into production.

To display UniMsgDlg, I needed to intercept the default the WinForms unhandled exception event handler and provide my own custom handler. This involves adding a delegate to the application’s ThreadException object soon after the program starts:

ThreadExceptionEventHandler hdl =
	new ThreadExceptionEventHandler(
	UniHandler);
System.Windows.Forms.Application.
	ThreadException += handler;
The parameter, UniHandler, passed to the event handler constructor is the name of the method in my code responsible for displaying UniMsgDlg that will be invoked for any unhandled exceptions, implemented as shown here:
protected virtual void UniHandler(object sender,
	ThreadExceptionEventArgs args)
{
	try
	{
		UniException uni = args.Exception as UniException;
		if ( uni != null )
		{
			Program.Instance.AppLog.WriteMsg(uni.ToString());

			UniMsg msg = Program.Instance.ErrMsgs.GetMsg(uni.MsgId);
			UniMsgDlg dlg = new UniMsgDlg(msg, uni.ResetAction, uni.Class,
				uni.Fmt, uni.Args);
			if ( dlg.ShowDialog() == DialogResult.Yes )
			{
				if ( uni.ResetAction == UniException.Reset.TermApp )
					Program.Instance.TerminateApp(false);
				else
					Program.Instance.ResetApp();
				}
			}
		}
	catch (Exception e)
	{
		FatalErrorAbend(“Program.UniHandler()”, e.Message);
	}
}
//full implementation given in Program.cs
The experience of implementing this code in TestWinApp made me aware that I needed to move this code from the application into the AppToolBox library as otherwise it would be duplicated for every application in which it was used. I needed some form of class for application specific behaviour. The ‘Application’ class provided by the FCL is sealed, so I created the abstract base class ‘Program’ from which I derived a class ‘TestWinApp’ in my test-bed application. I used the Singleton pattern to implement ‘TestWinApp’ as I wanted to ensure that there was only one such object in my application, accessed by a static variable named Instance. In this way ‘TestWinApp’ automatically added my Exception Handler to the application’s ThreadException object (through the base class) when Instance was being set as the program loaded:
public abstract class Program
{
	public abstract UniLog	AppLog { get; }
	public abstract UniMsgList ErrMsgs { get; }
	private static Program m_instance = null;

	protected static Program Instance
	{
		set
		{
			try
			{
				m_instance = value;
				ThreadExceptionEventHandler handler =
					New ThreadExceptionEventHandler(Program.Instance.UniHandler);
				System.Windows.Forms.Application.ThreadException += handler;
			}
			catch(Exception efinal)
			{
				Program.FatalErrorAbend(“Program.Instance-set”, efinal.Message);
			}
		}
	get
	{
		return m_instance;
	}
}
//full implementation given in Program.cs

public class TestWinApp : Program
{
	private UniMsgList m_UniMsgList;
	private UniLog m_UniEventLog;

	private TestWinApp() {}
	static TestWinApp()
	{
		try
		{
			Program.Instance = new TestWinApp();

			CultureInfo culture = null;
			if (Thread.CurrentThread.CurrentUICulture.Name == “de”)
				culture = new CultureInfo(“de”);
			TestWinApp.Instance.m_UniMsgList = niMsgList.LoadList(new Temp(),
					“TestWinApp.TestWinAppMsg.xml”, culture);

			TestWinApp.Instance.m_UniEventLog = new UniLog();
			TestWinApp.Instance.m_UniEventLog.Name = “TestWinApp”;
		}
		catch (Exception e)
		{
			Program.FatalErrorAbend(“TestWinApp.TestWinApp()”, e.Message);
		}
	}
	static public new TestWinApp Instance
	{
		get { return Program.Instance as TestWinApp; }
	}
//full implementation given in main.cs

Abort, Reset, or Terminate

It is essential to perform some form of reset during error processing so as to restore the program back to an operational state. In most cases there is no explicit action to take as throwing an exception will abort the operation and the WinForms unhandled exception processing will reset the application so it can continue to accept further user interactions. However, in certain error situations you may need to reset your own code and data. This is the purpose of the virtual method, ResetApp(). You might, for example, use ResetApp() to reinitialise the application’s document object and so drive a refresh of all the view objects, assuming your program implements some form of model-view-controller pattern. The highest level of reset is, of course, to terminate the program and this is the job of TerminateApp().

Error Logging

So far we’ve only looked at how UniHandler() processes errors from the Customer (user) perspective by displaying a transitory message. However, UniHandler() must also record the corresponding programmer message for subsequent analysis, which means saving the error in some form of persistent storage. I decided to use the built-in logging facility of the operating system for recording programmer messages because it is supported by a comprehensive range of tools and the mechanism is well understood by support staff.

To write an entry in the log you just need to create an instance of the framework class EventLog and then invoke its WriteEntry() method, passing the message as a string and the type of entry (error, warning, information, etc) as an enumeration. However, because I was driving my development by tests, I started by creating a simple test for my own UniLog class and then later evolved this class to contain an EventLog object which was instantiated as the log was named:

public class UniLog
{
	private EventLog m_EventLog;
	private string m_Name;

	public UniLog()
	{
	}

	public virtual string Name
	{
		get { return m_Name; }
		set
		{
			if ( m_Name == null )
			{
				m_Name = value;
				m_EventLog = new EventLog(“Application”, “.”, m_Name);
			}
		}
	}
//full implementation given in UniLog.cs
I added a ‘get’ property called AppLog to my abstract Program class to ensure any application using this logging facility would have to implement a UniLog object that could then be accessed via the static Instance variable. This meant that adding logging to my exception handler could be achieved with a single line of code:
Program.Instance.AppLog.WriteMsg(
	uni.ToString());
The ToString() method in my UniException class was overridden to obtain the programmer’s message from the exception so it could be passed to UniLog’s WriteMsg() method for storage in the system’s Application log. To view theses log entries the support staff need only start the Event Viewer (Control Panel, Administrative Tools) and select the Application log in its tree view (see Figure 2), a job that could be done remotely if necessary.

Figure 2: The Programmer Message Displayed in Event Log
Figure 2: The Programmer Message Displayed in Event Log

Internationalization

Almost any kind of resource can be localized, not just xml, and you can also localize your code in order to display things like currency and dates in a way that is appropriate for your user’s culture. However, for this project I was just concerned with localizing the messages in TestWinAppMsg.xml. I started by writing a new test, one to confirm the correct localized xml resource has been loaded. The new test simply asserted that when the user culture is set to German then the message cause appears in German, otherwise it appears in English. The user’s cultural information is contained in the framework CultureInfo object which is accessed as follows:
System.Threading.Thread.
CurrentThread.CurrentUICulture
Although Windows sets the CultureInfo object according to the type of user culture specified when the operating system was initially installed on the PC, you can change this CultureInfo object to facilitate testing. Typically, you would create a new object passing to its constructor a string like “en-US” ( U.S. English) or “de” (German) to define the required culture:
[Test]
public void GetMsgMultiCultural()
{
	UniMsgList list = new UniMsgList();

	list = UniMsgList.LoadList(this, “AppToolBoxNUnit.Test.xml”, null);
	Assert.AreEqual(“Hello World!”, list.GetMsg(“A”).Cause, “english”);

	list.Clear();

	list = UniMsgList.LoadList(this, “AppToolBoxNUnit.Test.xml”,
			new CultureInfo(“de”));
	Assert.AreEqual(“hallo Welt!”, list.GetMsg(“A”).Cause, “german”);
		}
//full implementation given in UniMsgListFixture.cs
I added a further parameter to UniMsgList.LoadList() referencing this CultureInfo object so I could invoke the Assembly method GetSatelliteAssembly() to load the assembly containing the correct localised resources. The satellite assemblies are found in subdirectories immediately below the main application directory and to generate them all you need to do is create a file with the correct name, add it to the Project, set its build property to “embedded resource”, and then rebuild the solution.

To test localization of the messages for German speakers a new file was created called “Test.de.xml”, which was added to the project with the property “embedded resource” as described earlier. This allowed the test to pass so all that was left to do was to add some code in the WinTestApp constructor so German resources would be used in place of the default English ones:

CultureInfo culture = null;
if ( System.Threading.Thread.
		CurrentThread.CurrentUICulture.
		Name == “	de”)
	culture = new CultureInfo(“de”);
To complete the support for German users in WinTestApp, I created the message file “TestWinAppMsg.de.xml”, added it to the project, and then rebuilt the solution to embed this resource into the application’s German satellite assembly (see Figure 3). It really was that simple, in fact the only difficult part of supporting a new language was finding someone to whom I could send TestWinAppMsg.xml for translation.

Figure 3: Error Message Dialog for German Speakers
Figure 3: Error Message Dialog for German Speakers

Summary

I have shown in this article how a small amount of simple code can provide quite sophisticated error processing for a Windows Forms application. There are undoubtedly ways in which this solution could be improved, but hopefully it will encourage more awareness about the need to handle errors both from the perspective of the people who use our programs and those who maintain them.


Will Stott is a freelance consultant living in Zurich. He is an associate of Exoftware, a Dublin based company helping organisation to become more Agile in their software development practices. He can be contacted at [email protected].

Resources

  • vsj.co.uk – to download the article’s source-code and other material needed to build TestWinApp and its libraries.
  • www.winfitrunner.org – site of the project that inspired this article, where you can download an example of error processing in a typical application.
  • www.nunit.org – where to download the latest version of NUnit in order to run the unit tests contained in the article’s source-code.
  • C# Cookbook – Teilhet & Hilyard, O’Reilly, 2004 – contains an interesting chapter on Exception handling in .NET as well as many other useful code fragments.

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“Computer science education cannot make anybody an expert programmer any more than studying brushes and pigment can make somebody an expert painter” - Eric Raymond