Custom Exception Handler Example: LocalizedReplaceHandler
Introduction
Let’s face it; we can definitely improve on the usability
around creating your own ExceptionHandlers, especially if you want to have a
rich tooling experience in the EntLib configuration console. I’ve decided to
create an example ExceptionHandler that can serve you as a reference for your
own ExceptionHandlers. The LocalizedReplaceHandler is an
ExceptionHandler that
works almost identically to the included ReplaceHandler, but it includes some
extra configuration options for using a localized message from a resource file
as opposed to a static message defined in configuration.
Simple vs. Complex Configuration
This is not an example of simple configuration. By “simple”
I mean by using the CustomHandlerData class for storing generic configuration
items in a NameValueCollection. This also means that you load a CustomHandler
via the config tool, which prompts you to load your assembly with your
ExceptionHandler. By “complex” I mean that you have rich tooling support
(validation, strongly typed properties, dialogs for complex types, etc.) as
well as a registered type in the command menu (i.e. instead of choosing
CustomHandler, your handlers name will appear as another menu item). This example
uses complex configuration which is quite a bit more difficult than simple
configuration. However, the end result is usually worth the huge usability
improvements, and hopefully my example will help you along the way.
A Quick Example of Simple Configuration
Before we get into the LocalizedReplaceHandler, here’s a
quick example of a custom ExceptionHandler that expects a property named
MyProperty. Take note that there’s no way to enforce that this property was
entered in configuration.
public class CustomHandler : ExceptionHandler
{
// This holds our configuration data
CustomHandlerData configurationData;
public override Exception HandleException(Exception exception, string policyName, Guid handlingInstanceId)
{
// Retrieve "MyProperty" from the NameValueCollection
string myPropertyValue = configurationData.Attributes["MyProperty"];
// TODO: Handling logic
return exception;
}
public override void Initialize(ConfigurationView configurationView)
{
// Get the ExceptionHandling specific view
ExceptionHandlingConfigurationView view = (ExceptionHandlingConfigurationView)configurationView;
// Get the configuration data
configurationData = (CustomHandlerData)view.GetExceptionHandlerData(base.CurrentPolicyName, base.CurrentExceptionTypeName, base.ConfigurationName);
}
}
In the configuration tool, we can configure our exception
handler by creating a new CustomHandler, loading our assembly in which our
handler exists, and selecting the handler.
Complex Configuration with the LocalizedReplaceHandler
There are three high level aspects involved with creating an
ExceptionHandler with rich configuration tool support:
- The
ExceptionHandler. Of course, before we can build the configuration, we need to have an
ExceptionHandler. This class must implement
IExceptionHandler (which includes deriving from the abstract ExceptionHandler
class).
- The
ExceptionHandlerData class. This is your strongly typed configuration data
class. This class must be XML serializable. This class must derive from
ExceptionHandlerData.
- The
ExceptionHandlerNode. This class defines the design time interaction with
the ExceptionHandlerData object. This is any class that derives from
ExceptionHandlerNode. Usually this class is kept in a separate “design”
assembly. This is because there is tool specific code and dependencies
that you will not want to deploy on a production server. This is also why
you see so many “design” assemblies within Enterprise Library.
ExceptionHandler
The ExceptionHandler will be similar to the CustomHandler
that was shown for the simple configuration example. The key difference is that
instead of loading CustomHandlerData from the ConfigurationView, we load our
data class, LocalizedReplaceHandlerData. The source is too big to paste here
but you can download the sample solution and study it for yourself.
ExceptionHandlerData
Here is a truncated version of LocalizedReplaceHandlerData:
[XmlRoot("exceptionHandler", Namespace=ExceptionHandlingSettings.ConfigurationNamespace)]
public class LocalizedReplaceHandlerData : ExceptionHandlerData
{
private string exceptionMessageToken;
private string replaceExceptionTypeName;
private string resourceBaseName;
private string resourceAssemblyName;
private string defaultMessage;
/// <summary>
/// Initialize a new instance of the <see cref="LocalizedReplaceHandlerData"/> class.
/// </summary>
public LocalizedReplaceHandlerData() : this(string.Empty)
{
}
/// <summary>
/// Initialize a new instance of the <see cref="LocalizedReplaceHandlerData"/> class with a name.
/// </summary>
/// <param name="name">
/// The name of the <see cref="LocalizedReplaceHandlerData"/>.
/// </param>
public LocalizedReplaceHandlerData(string name) : this(name, string.Empty, string.Empty)
{
}
/// <summary>
/// Initialize a new instance of the <see cref="LocalizedReplaceHandlerData"/> class with a name, exception message, and replace exception type name.
/// </summary>
/// <param name="name">
/// The name of the <see cref="LocalizedReplaceHandlerData"/>.
/// </param>
/// <param name="exceptionMessage">
/// The exception message replacement.
/// </param>
/// <param name="replaceExceptionTypeName">
/// The fully qualified assembly name the type of the replacing exception.
/// </param>
public LocalizedReplaceHandlerData(string name, string exceptionMessage, string replaceExceptionTypeName) : base(name)
{
this.exceptionMessageToken = exceptionMessage;
this.replaceExceptionTypeName = replaceExceptionTypeName;
}
/// <summary>
/// Gets or sets the optional resource token for exception message replacement.
/// </summary>
[XmlAttribute("exceptionMessageToken")]
public string ExceptionMessageToken
{
get { return exceptionMessageToken; }
set { exceptionMessageToken = value; }
}
// (Other properties ommitted for brevity)
}
There are a few things that you should note. First, you must
specify the XmlRoot at the top of the class. Second, you should implement all
three constructor overloads. Finally, you should specify the XmlAttribute for
each property with the property name starting with a lower case. This is so
your node names conform to standard Xml naming conventions. Another point: I
usually like to put configuration classes in their own namespace, but it’s not
required.
Setting Up the Design Project
This is where things get a bit hairy. Before we can create
our ExceptionHandlerNode, we need to setup a design project to store all of our
design time code. Simply create a normal class library project. You’ll then
need to create a DesignManager. The DesignManager is a class that’s responsible
for managing all of your nodes, how they open, save, interact with menu
commands, etc. Fortunately the DesignManager we’ll be creating is relatively
simple as the ExceptionHandling block’s DesignManager does a most the work for
us already. There are two tasks that we
need to perform for each ExceptionHandlerNode that we create. The first is to
register our ExceptionHandlerData as an XmlIncludeType. This allows the
configuration tool to know about our data class so that it can properly
serialize/deserialize it. The second is to register our ExceptionHandlerNode
with the context menu so that it will show up as an option to our user. For
now, let’s just create the shell of the DesignManager, with the XmlIncludeType
for the LocalizedReplaceHandlerData class.
public class DesignManager : IConfigurationDesignManager
{
/// <summary>
/// <para>Registers the <see cref="LocalizedReplaceHandlerNode"/> in the application.</para>
/// </summary>
/// <param name="serviceProvider">
/// <para>The a mechanism for retrieving a service object; that is, an object that provides custom support to other objects.</para>
/// </param>
public void Register(IServiceProvider serviceProvider)
{
RegisterNodeTypes(serviceProvider);
RegisterXmlIncludeTypes(serviceProvider);
}
public void Open(IServiceProvider serviceProvider)
{
// Do nothing as the EHAB design manager takes care of this.
}
public void Save(IServiceProvider serviceProvider)
{
// Do nothing as the EHAB design manager takes care of this.
}
public void BuildContext(IServiceProvider serviceProvider, ConfigurationDictionary configurationDictionary)
{
// Do nothing as the EHAB design manager takes care of this.
}
private static void RegisterXmlIncludeTypes(IServiceProvider serviceProvider)
{
IXmlIncludeTypeService xmlIncludeTypeService = ServiceHelper.GetXmlIncludeTypeService(serviceProvider);
xmlIncludeTypeService.AddXmlIncludeType(ExceptionHandlingSettings.SectionName, typeof(LocalizedReplaceHandlerData));
}
private static void RegisterNodeTypes(IServiceProvider serviceProvider)
{
// TODO: Finish once our ExceptionHandlerNode is complete.
}
}
There’s one more thing to do before we continue. We must
register our DesignManager with the configuration tool. We do this by adding
the following attribute to the AssemblyInfo.cs file:
[assembly : ConfigurationDesignManager(typeof(DesignManager))]
This allows the configuration tool to discover our
DesignManager and therefore discover the nodes that we create.
ExceptionHandlerNode
Now that we have our design project setup, we can create the
LocalizedReplaceHandlerNode. Remember that this class is used not only to
control the design time experience, but also to bind data input from the UI to
the underlying data class. Therefore, we must specify which data class to use. This
is done by specifying two different constructors. We need one constructor which
creates a default instance, and another which allows a specified instance.
public class LocalizedReplaceHandlerNode : ExceptionHandlerNode, ITypeDependentExceptionHandler
{
private LocalizedReplaceHandlerData handlerData;
/// <summary>
/// Constructs the node with default values.
/// </summary>
public LocalizedReplaceHandlerNode() : this(new LocalizedReplaceHandlerData("Localized Replace Handler", string.Empty, string.Empty))
{
}
/// <summary>
/// Constructs the node with config data.
/// </summary>
/// <param name="handlerData">The config data to initialize this node.</param>
public LocalizedReplaceHandlerNode(LocalizedReplaceHandlerData handlerData) : base(handlerData)
{
this.handlerData = handlerData;
}
}
Next we need to define all of the properties that will
appear in the property grid. Each property can have a variety of attributes
that allow for validation as well as specific editors for use within a
PropertyGrid. For example, here is the code for the ExceptionMessageToken
property:
/// <summary>
/// Gets or sets the optional resource token for exception message replacement.
/// </summary>
[Description("The optional resource token for exception message replacement.")]
[Category("General")]
public string ExceptionMessageToken
{
get { return handlerData.ExceptionMessageToken; }
set { handlerData.ExceptionMessageToken = value; }
}
Notice the Description and Category attributes. These are
just a couple of the PropertyGrid attributes that you can use. For validation,
you can set properties as required, or specify a range, for example. See the
Microsoft.Practices.EnterpriseLibrary.Configuration.Design.Validation namespace
for the included validation attributes that you can use. You can of course
extend this to perform your own validation.
Once all of the properties are created, the node needs to be
registered with the DesignManager. This is done within the RegisterNodeTypes
method. Using a NodeCreationService, we
can create a NodeCreationEntry which allows us to specify the type of node we
are creating, the type of command we want (in this case the
AddChildNodeCommand, since we want this node to be added as a child node to the
ExceptionType node), the mapped data class, as well as the text for the
command.
private static void RegisterNodeTypes(IServiceProvider serviceProvider)
{
INodeCreationService nodeCreationService = ServiceHelper.GetNodeCreationService(serviceProvider);
Type nodeType = typeof(LocalizedReplaceHandlerNode);
NodeCreationEntry entry = NodeCreationEntry.CreateNodeCreationEntryWithMultiples(
new AddChildNodeCommand(serviceProvider, nodeType),
nodeType,
typeof(LocalizedReplaceHandlerData),
"Localized Replace Handler"
);
nodeCreationService.AddNodeCreationEntry(entry);
}
Final Steps
After compiling the solution you have a couple of options
for using your assemblies with the configuration tool and your application. You
may wish to load your assemblies in the GAC so that the references are easy to
find. If not, you will need to copy your assemblies to the configuration tool’s
directory, as well as the bin directory of any application that will actually
use the handler. Either way, the configuration tool will automatically find
your DesignManager which will then add your node and commands into the tool.
When you’re done, the final product should look something like this:
So what are you waiting for?
Download the sample source code and try it out for yourself!