Wednesday, May 11, 2005 - Posts

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:

  1. 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).
  2. The ExceptionHandlerData class. This is your strongly typed configuration data class. This class must be XML serializable. This class must derive from ExceptionHandlerData.
  3. 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!

with 6 Comments