VSM Cover Story

Create a Data-Driven Messaging System

Hard-coded messages don't scale well, and .NET's inherent error messages are user-unfriendly. Learn how to create a more scalable and user-friendly data-driven messaging system.

TECHNOLOGY TOOLBOX: VB.NET, C#, Other: XML

Displaying messages to users of an application is something every developer needs to do. There are many ways to display such messages, some better than others. For example, one approach is to hard code a message that you pass to the Show method: MessageBox.Show("Data has been Saved"). There are several problems with this approach. First, you would end up writing an if statement for each language if you need to internationalize your application. Second, each time you need to change the message, or add a new language, you need to recompile and redistribute your application. Another kind of hard-coded message is one that's generated by the .NET runtime itself in the form of exception messages. You might use MessageBox.Show(ex.Message) within a catch block to display a .NET exception message on the screen, but most exception messages are simply not meant for an end user -- in fact such messages aren't even decipherable by the developer, much of the time. In this article I'll show you how to create a data-driven messaging system that provides a better approach for displaying messages to end users.

In a typical application, you have many messages you need to display. Labels that describe the input for a textbox control, or a message box control that simply says: "Data has been Saved." You also have longer messages such as, "You cannot delete an order that has order detail lines. Please delete the detail lines first." You also might have messages that are returned from .NET itself when an unexpected exception occurs in an application. Exception messages should never be displayed to an end user, but should be published to a location where a developer can retrieve that message.

I won't focus on setting labels or publishing exception messages. Labels that preface text boxes should be set using the built-in resource files that Microsoft has made available in .NET. Exception publishing can be accomplished by using the Microsoft Enterprise Library, the PDSA .NET Base Framework, Log4Net, or any other tools available for free or commercially. Instead, I'll focus on how you can present messages to a user and how to display standard error messages based on exceptions that occur in your application.

There are many types of "messages" that users are presented with in an application. Messages inform the user of what happened, or what is happening, within an application. No one is expecting a developer to be an English major, but there are a few rules you should attempt to follow when writing messages: Use complete sentences; end each sentence with a period or other appropriate punctuation; use good capitalization; use a conversational tone; and be sure to run a spell check against your messages.

If you're like me, you will probably want to enlist some help in writing good messages. The person you want to enlist is probably not a programmer and will have a hard time looking at source code. Thus, you should store your messages in an XML file or other data storage location that will make it easier for a non-programmer to view and tweak the messages. Having these messages stored externally will also make translation to a different language easier, as well.

Displaying Exception Messages
You don't typically want to display a .NET exception message to the user. This information would overwhelm the typical user, and it can give valuable information to a hacker looking to exploit your system and potentially gain access to your application or network. The .NET exception messages can contain table names, connection strings, user names, or other potentially revealing information. What you need to do is to come up with a good message to display to the user, one that tells them that something happened, but does not reveal unnecessary information.

Each exception that you trap, or might potentially trap, must have a corresponding "nice-message." If the same exception could occur in different parts of the program, you should display the same nice error message to the user. Again, this is much easier to do if you're not hard coding these messages in your application. For example, an ArgumentNullException exception might generate a message like "Value cannot be null. Parameter name: FirstName." This is fine for us as programmers; however, a user would be clueless as to how to act on this information. Instead, you could have your own message defined in an external storage location such that whenever an ArgumentNullException message is generated, you could retrieve a much friendlier version of this message: "We're sorry, but a value that was supposed to be sent to the application was not supplied. We are therefore informing our developers of where this happened, and we will get you a new version of this software ASAP." After displaying this message, the .NET exception could then be sent to the developer through one of the tools mentioned previously.

I've stated that an XML file is a good way to store all your messages. This XML file contains messages that you might use in a typical application (see Listing 1). You will need to change and add messages as appropriate for your specific application(s). In a production application, you could store this XML file in a shared folder on your network drive to make it easy to update the messages. There's no language field in this sample file; however, you could add that easily and modify the sample code to display messages in English, French, German, and so on. You now have an XML file with various messages. Next, you need to create a class that is responsible for retrieving those messages for your application.

As with any data you need to retrieve from a storage location, you should create a class to retrieve messages. The class used in this article, named Message, is what you use to retrieve the data from the XML file presented in Listing 1. Using a class to wrap up the storage location helps you keep the programmer API consistent, even if you change the location of the back-end data storage (see Table 1).

Retrieving Messages
The next step is to create the GetMessage() method, which lets you retrieve messages based on various inputs (see Listing 2). This method performs the work for all of the other overloaded messages.This method lets you retrieve a message from your message storage location by any combination of ClassName, MessageName, MessageNumber, and/or MessageID. You can pass one, all, or any combination of these parameters to retrieve the appropriate message. You use the appropriate overload to call the GetMessage method, instead of just passing an empty string or a zero to the appropriate parameters of this method. Once you pass in the appropriate parameters, a method named GetMessageFromStorage() is called to retrieve the message. The version of the GetMessageFromStorage() method in this article uses LINQ to XML to retrieve the message based on the parameters passed in.

The GetMessage() method sets the appropriate internal fields of the class based on the parameters you pass in, then makes the call to the GetMessageFromStorage() method (which I'll describe shortly). If no message is returned from the GetMessageFromStorage() method, the value in the DefaultMessage property is returned to the user. The DefaultMessage property is hard coded to a default value when an instance of this class is created, but you can set it for each application, as appropriate.

This code lets you retrieve a specific message to display to the user:

// C#
Message pm;
pm = new Message(@"D:\Messages.xml");
// Method 1
MessageBox.Show(pm.GetMessage(
  "frmMain", "MainMessage", 1));
// Method 2
MessageBox.Show(pm.GetMessage(
  "MainMessage", 2));
// Method 3
MessageBox.Show(pm.GetMessage(
  "MainMessage"));
// Method 4
MessageBox.Show(pm.GetMessage(4));
// Method 5
MessageBox.Show(pm.GetMessage(
  "frmMain", "MainMessage"));

' VB.NET
Dim pm As Message
pm = New Message("D:\Messages.xml")
' Method 1
MessageBox.Show(pm.GetMessage( _
  "frmMain", "MainMessage", 1))
' Method 2
MessageBox.Show(pm.GetMessage( _
  "MainMessage", 2))
' Method 3
MessageBox.Show(pm.GetMessage( _
  "MainMessage"))
' Method 4
MessageBox.Show(pm.GetMessage(4))
' Method 5
MessageBox.Show(pm.GetMessage( _
  "frmMain", "MainMessage"))

Each overload of the GetMessage() method helps you retrieve a specific message in the Messages.xml file. Using an XML file gives you more flexibility than a normal resource file. You can even use the Messages.xml to replace messages returned from the .NET Framework.

Of course, this is not the only usage of the Message class. You can also use this class with an exception object to retrieve a message based on the type of an exception.

There are many types of exceptions that can occur in a .NET application, from "System.ArgumentNullException" to "System.DivideByZeroException" to "System.Data.SqlClient.SqlException." You want to create a specific user-friendly error message in your message store that makes each error message easy for the user to digest. The overloaded GetMessage() method is the one responsible for checking exception types and returning an appropriate error message to the user (see Listing 3). In the overloaded version of the GetMessage() method, the switch/select case statement is used to determine the type of exception. Depending on what type of exception is passed in, the appropriate call to the overloaded GetMessage() method is used to retrieve a message from the back-end data store. You might also use the exception type in combination with a ClassName to further refine the message that's returned.

Note that the default/Case Else statement passes on the exception type to the GetMessage() method. So, if you have a divide by zero exception, then the string "System.DivideByZeroException" is what is passed to the GetMessage() method. This means that you need to create an appropriate entry in your message store for returning a message for this exception. For example, this entry might look like this:

<Message>
<MessageID>56</MessageID>
<MessageName>System.DivideByZeroException
  </MessageName>
<MessageNumber></MessageNumber>
<ClassName></ClassName>
<Message>
You have attempted to divide a number by zero. 
Please re-enter your divisor and try again with 
a non-zero number.
</Message>
</Message>

This message will display whenever this error occurs in your code. This keeps the messages shown to the user consistent and easy-to-follow.

Handle SQL Exceptions
When you deal with a relational database, you might hit a case where you have a foreign-key constraint that can cause an exception to occur if you attempt to insert, update, or delete a related record. For example, if you attempt to delete an Order record that has related Order Detail records in the Northwind database, then the .NET exception message that's generated looks like this:

"The DELETE statement conflicted with the REFERENCE constraint "FK_Order_Details_Orders". The conflict occurred in database "Northwind", table "dbo.Order Details", column 'OrderID'. The statement has been terminated."

This isn't something that an average user is going to be able to decipher. This message also shows way too much information about the internals of the system that a hacker could exploit.

When an exception of the type "SqlException" is detected in the GetMessage() method, the code loops through all SqlException objects within the Errors collection to retrieve the SQL error number from each object. This error number is used in combination with the class name and the message name "SqlException" to retrieve a message from the GetMessage() method.

You can use the SqlNumber from this foreign key error (547, in this case) as a lookup into the XML file, along with the message name "SqlException" and any class name you pass in. For example, you can use the combination of "SqlException", "frmMain", and 547 to retrieve the specific message in the XML file:

<Message>
<MessageID>7</MessageID>
<MessageName>SqlException</MessageName>
<MessageNumber>547</MessageNumber>
<ClassName>frmMain</ClassName>
<Message>
You are attempting to delete an order that has
order detail records. Please delete the order detail
records first.
</Message>
</Message>

This message is much better suited to display to the user than the .NET exception message. The code used to produce the .NET exception message looks like this:

// C#
private void ExceptionClassNameDisplay()
{
SqlCommand cmd = null;
string strSQL;
// Attempt to delete an order 
// with Foreign Key Reference
strSQL = "DELETE FROM Orders WHERE 
   OrderID = 10253";
try
{
cmd = new SqlCommand(strSQL);
cmd.Connection = new SqlConnection(
   txtConnectString.Text);
cmd.Connection.Open();
cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
PDSAMessage pm;
pm = new PDSAMessage(
AppConfig.MessagesXmlFile);
MessageBox.Show(pm.GetMessage(ex, this.Name));
}
}

' VB.NET
Private Sub ExceptionClassNameDisplay ()
Dim cmd As SqlCommand
Dim strSQL As String
' Attempt to delete an order 
' with Foreign Key Reference
strSQL = "DELETE FROM Orders WHERE _
   OrderID = 10253"
Try
cmd = New SqlCommand(strSQL)
cmd.Connection = New SqlConnection( _
   txtConnectString.Text)
cmd.Connection.Open()
cmd.ExecuteNonQuery()
Catch ex As Exception
Dim pm As PDSAMessage
pm = New PDSAMessage( _
   AppConfig.MessagesXmlFile)
' Display user error
MessageBox.Show(pm.GetMessage(ex, Me.Name))
End Try
End Sub

The final step is learning how to use the GetMessageFromStorage() method to retrieve a specific message from the XML file.

Retrieve a Specific Message
The GetMessageFromStorage() method does all of the work of retrieving the messages for you. Sample code that illustrates this is available in (Listing A). I gave the method this name because you can override the method with your own code and retrieve the data from another data storage location.

The first thing the GetMessageFromStorage() method does is set an enumeration based on the internal fields that got set by the GetMessage() method. The enumeration is then used in the switch/select case statement to determine which LINQ to XML statement to use to retrieve the appropriate message from the XML file. Each LINQ to XML statement looks for different messages based on the variables that were set in the GetMessage() method.

Note that you must make sure you have a valid XML file that has been checked against a schema. LINQ to XML requires a valid XML file to work correctly. If you don't have a valid XML file -- if elements or attributes are missing, for example -- then you will get some "not-so-nice" error messages that will be almost impossible to debug. What's more frustrating is that the same code will work in VB.NET, but C# will fail with a less-than-helpful error message. As you can imagine, this caused this author more than an hour or two of frustration!

This might make you wonder: Why not just use a resource file for all of these messages? You could. However, I find that I get more flexibility from an XML file. A resource file allows you to have a single "key" only as a means to retrieve a string. This means you have only one way to retrieve a message. I have found that I need a little more flexibility, and I want to be able to retrieve my messages using multiple "keys" as outlined in this article. I use a combination of resource files and the data-driven message system in my applications. Resource files are great for labels or other UI-related messages, but the data-driven message system is wonderful for all other messages.

A message storage and retrieval system is ideal for centralizing your messages. This approach lets you customize messages without having to recompile and redistribute your application. The data-driven approach also allows you to create messages in different languages for globalized applications. Remember that you don't have to use an XML file as your data storage; you could use a resource file, a local JET database table, or even store messages in a SQL Server or Oracle table. Be aware, however, that if you are unable to get to your database table, you won't be able to retrieve your messages. That's why keeping them in a local (or network accessible) data store is probably your best bet.

comments powered by Disqus

Featured

Subscribe on YouTube