Using Enterprise Library in ASP.NET 2.0 - Part 5

"Adding Configuration Design Support to a Custom Provider"

©2006 Alexander Homer – alex@stonebroom.com   

 

This series of articles looks at the patterns & practices Enterprise Library tools, and how you can use them in your ASP.NET applications. The five articles are:

 

·         Part 1 - An Introduction to Enterprise Library

·         Part 2 - Working with Enterprise Library in ASP.NET

·         Part 3 - Using Enterprise Library in ASP.NET Partial Trust Mode

·         Part 4 - Extending Enterprise Library with Custom Providers

·         Part 5 - Adding Configuration Design Support to a Custom Provider

 

 

Enterprise Library from the Microsoft patterns & practices (p&p) group provides a range of application blocks and features that make it easier to accomplish complex tasks such as accessing databases, caching and encrypting data, handling exceptions, and generating Event Log messages. It demonstrates best practice in design and implementation, and is supplied in source code form so that you can learn from it, and also adapt it to suit your own specific requirements.

 

In a previous article (see Extending Enterprise Library with Custom Providers), you saw how easy it is to build a custom provider for an Enterprise Library application block. The example shown was a custom backing store provider for the Caching Application Block, but the principles for creating providers that extend Enterprise Library are the same irrespective of the block that you need to extend.

 

However, as you saw in the previous article, custom providers are effectively second-class citizens within Enterprise Library because you can configure them only as an instance of the pre-defined custom provider type, and you must use a NameValueCollection as the Attributes property in the Configuration Console to pass values from the configuration into the provider at runtime. This is unintuitive and error-prone because the user has no visual indication of the number of name/value pairs required, or the name and type of each one. In addition, the Configuration Console cannot enforce rules such as which values are required, which are optional, and what the valid data type is for each one.

Configuration Console Design Support

The good news is that you can add classes and code to your provider that allows it to fully integrate with the Configuration Console, and become a first-class citizen within Enterprise Library. To achieve this requires three basic conditions to be satisfied:

 

·         Your provider must expose a constructor that takes as parameters the values the user will configure in the console

·         You must create one or more classes that store and expose the configuration data in the configuration file to your provider

·         You must create one or more classes that represent the nodes in the console tree view that the console uses during the configuration process

 

In addition, you must specify text strings for display in the console during the configuration process, create a class that ObjectBuilder uses to generate your custom provider configuration, and you must modify the classes that add items to the menus in the configuration console and handle user interaction with these menus. It all sounds like a daunting prospect and a complicated process, but in fact it is relatively simple as long as you take an ordered and careful approach to modifying the existing Enterprise Library classes.

 

In this article, you'll see how to adapt the custom file caching provider created in the previous article to add Configuration Console design support. Figure 1 shows the result, and you can see that the menu to add a Cache Manager now contains the Custom File Cache Storage option.

 

Figure 1 - Adding a Custom File Cache Storage provider to the Caching Application Block

 

This makes the process much easier and more intuitive (and discoverable) when selecting a custom provider. There is no need to locate the containing assembly and specify the type because the Configuration Console knows the type of the provider. In addition, the Configuration Console can display a list of the properties required for the provider, and use specific controls or dialogs to make setting the properties easier and less error-prone. In Figure 2, you can see how the user can set the CacheFilePath property of the custom provider using a Windows Save File dialog.

 

Figure 2 - Specifying the CacheFilePath property for the Custom File Cache Storage provider

The Process for Adding Configuration Design Support

The process for adding design support involves five main tasks:

 

·         Determine the provider configuration requirements and decide which properties/attributes the configuration file will contain.

 

·         Modify your custom provider class. Change the constructor parameters in your custom provider class, and make any other modifications required, so that it accepts separate parameters for each configuration attribute rather than receiving them all as a single NameValueCollection. Also change the configuration attribute on your class to specify the configuration data class type that your class will use within the Configuration Console.

 

·         Create the configuration data and configuration node classes that describe the custom provider (including the attributes it requires), an assembler class that manages the build process for the class in ObjectBuilder, and classes that describe the properties for the provider and the way that the console exposes them for editing.

 

·         Edit the resources file in the Design section to add entries for the text and commands displayed by the console for this provider node

 

·         Execute node and command registration by editing the block-specific class that inherits from NodeMapRegister so that it registers the new provider node with the console, and the block-specific class that inherits from CommandRegister so that it registers the strings in the resources file as commands the console will display when configuring the new provider.

 

The following sections explore each of the stages listed above. Note that a simple provider such as the custom cache backing store described in this article requires only a single configuration data class and a single configuration node class. You only need more if your provider needs to expose configurable child nodes in the console, and therefore requires parent/child configuration settings (such as multiple configurable paths for a file caching provider).

Determining the Provider Configuration Requirements

The first step is to decide what configuration values you need to persist for your provider. These are the attributes that will appear in the <add> element for the provider, excluding the type and name attributes that the configuration system automatically provides through the base classes you will extend. The values will appear as properties in the Configuration Console (from the configuration node classes you create), and be passed to your class constructor at runtime (through the configuration data classes you create).

 

The example cache backing store provider requires only one property - a String that is the full path to the folder where it will create the cache files. This property is persisted in the configuration file using the path attribute. You can persist any value type that you need, and the configuration system will convert it to the correct type at runtime. If you need to persist objects (such as child node types and their values), you must create separate configuration node and configuration data classes for these. Take a look at the way that the Caching Application Block and the other application blocks provide classes like this in their Configuration and Design subfolders for more details.

Modifying the Custom Provider Class

Modifications to the existing provider class are relatively simple. In this example, the class name has been changed so that you can install both the basic custom provider from the previous article and this design-configured provider. The class name is now CustomFileBackingStore. The class that will provide the runtime configuration data is named CustomFileCacheStorageData. You will see details of this class later. So, the provider class requires a ConfigurationElementType attribute that specifies the configuration data class:

 

[ConfigurationElementType(typeof(CustomFileCacheStorageData))]

public class CustomFileBackingStore : BaseBackingStore

...

 

The only other change is to the provider class constructor. Now it must accept parameters that are of the correct typed value. In the example, the only parameter required is the path to the cache file folder:

 

public CustomFileBackingStore(String cacheFilePath)

{

 

  // store the path and filename provided by the parameter

  if (cacheFilePath != String.Empty)

  {

    filePath = cacheFilePath;

  }

  else

  {

    throw new Exception("Error in application configuration, '"

                      + filePathConfigurationName + "' attribute not found");

  }

}

 

The rest of the code is the same as before. Note that the constructor still checks that there is a value for the path attribute in the configuration file (which appears as the cacheFilePath parameter to the constructor). The Configuration Console will force users to provide this attribute/property, but remember that users may edit the configuration file manually, outside of the console, and so you should still check that any required values are present within your constructor.

Creating the Configuration Data and Configuration Node Classes

The provider relies on a configuration data class that exposes the configuration at runtime. You should create a new class within the Configuration subfolder of the block you are extending, and add the required namespace references. For the example caching backing store provider, you need these references:

 

using System.Configuration;

using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;

using Microsoft.Practices.EnterpriseLibrary.Common.Configuration.ObjectBuilder;

using Microsoft.Practices.EnterpriseLibrary.Caching.BackingStoreImplementations;

using Microsoft.Practices.ObjectBuilder;

...

 

The usual approach is to use this file to hold both the configuration data class and its associated assembler class. The configuration data class carries an Assembler attribute that specifies the assembler class that Object Builder uses to create the configuration data class instance. 

 

The next listing shows the configuration data class, which should inherit from a suitable base class such as, in this case, CacheStorageData. It also carries the Assembler attribute that specifies the class CustomFileBackingStoreAssembler. Inside the configuration data class, you must provide a default constructor that takes no parameters, and a constructor that takes the name parameter (the name of the custom provider), and any properties the provider requires. In the case of the example provider, the only property is the path to the cache file folder: 

 

...

namespace Microsoft.Practices.EnterpriseLibrary.Caching.Configuration

{

  [Assembler(typeof(CustomFileBackingStoreAssembler))]

  public class CustomFileCacheStorageData : CacheStorageData

  {

    private const string pathNameProperty = "path";

 

    public CustomFileCacheStorageData()

    {

    }

 

    public CustomFileCacheStorageData(string name, string pathName)

      : base(name, typeof(CustomFileBackingStore))

    {

      this.CacheFilePath = pathName;

    }

    ...

 

Notice that the code above saves the provider-specific parameter value in a local variable so that it can make it available as a property. The configuration data class must expose the properties of the provider so that the configuration system and the provider can read and set them. For each property, add a ConfigurationProperty attribute that makes that property visible in the Configuration Console, specifies the name of the attribute in the configuration file, and indicates if the property is optional or required. In the case of the example provider, there is only one property named CacheFilePath. The local variable pathNameProperty holds the attribute name ("path"), and this property is required. The value for this property is stored in the collection of properties in the base class, and so the property accessor indexes into this collection to retrieve and set the value:

 

    ...

    [ConfigurationProperty(pathNameProperty, IsRequired = true)]

    public string CacheFilePath

    {

        get { return (string)base[pathNameProperty]; }

        set { base[pathNameProperty] = value; }

    }

  }

  ...

Adding an Assembler Class

The assembler class, within the same file, must implement the IAssembler interface, which has a single method named Assemble. Your method implementation receives a range of variables that the configuration system makes available to the constructor of the concrete implementation of the IAssembler interface within the core Enterprise Library configuration system. The method code casts this to the configuration data type CustomFileCacheStorageData, and then creates an instance of your provider class by callings its constructor and specifying the values for its properties using the values exposed by the configuration data class

 

  ...

  public class CustomFileBackingStoreAssembler

               : IAssembler<IBackingStore, CacheStorageData>

  {

    public IBackingStore Assemble(IBuilderContext context,

                                  CacheStorageData objectConfiguration,

                                  IConfigurationSource configurationSource,

                                  ConfigurationReflectionCache reflectionCache)

    {

      CustomFileCacheStorageData castedObjectConfiguration

        = (CustomFileCacheStorageData)objectConfiguration;

 

      IBackingStore createdObject

        = new CustomFileBackingStore(castedObjectConfiguration.CacheFilePath);

 

      return createdObject;

    }

  }

 

}

 

Building the Configuration Node Class

The Configuration Console uses the configuration node class for your provider to create the node in the configuration tree view, and to present the properties for the provider to the user for editing. You should create this class within the Configuration\Design subfolder of the block you are extending.  However, in the Enterprise Library Visual Studio solution that opens from your Start menu, there is a separate project for the Design features of each application block. Therefore, if you are using this solution, you should create your configuration node class within that project.

 

Then add the required namespaces to your class. For the example provider, you need the following:

 

using System.ComponentModel;

using System.Drawing.Design;

using Microsoft.Practices.EnterpriseLibrary.Configuration.Design.Validation;

using Microsoft.Practices.EnterpriseLibrary.Caching.Configuration.Design.Properties;

using System;

using Microsoft.Practices.EnterpriseLibrary.Configuration.Design;

...

 

Your configuration node class should inherit from an appropriate storage node base class, as shown in the next listing, depending on the block you are extending. In the case of the example provider, the appropriate base class is CacheStorageNode.

 

The configuration node class must also expose constructors that allow the Configuration Console to create instances of the class with default values for its properties, and with ready-configured values if the user is opening an existing configuration that contains the custom provider. For the example provider, the default constructor calls the constructor of the base class specifying the default name for this node (extracted from the resources file), and an empty String for the CacheFilePath property: 

 

...

namespace Microsoft.Practices.EnterpriseLibrary.Caching.Configuration.Design

{

  public class CustomFileCacheStorageNode : CacheStorageNode

  {

    private string pathName;

 

    public CustomFileCacheStorageNode()

           : this(new CustomFileCacheStorageData(Resources.DefaultFileCacheNodeName,

                                                 string.Empty))

    {

    }

    ...

 

If the user opens an existing configuration containing this provider, the second constructor takes a reference to the configuration data class containing the existing configuration information, checks that the reference is not null, and sets the Name property of the underlying base class (CacheStorageNode) to the name specified in the existing configuration for this provider. Then it sets the values of all the provider properties using the values in the existing configuration (there is only one property for the example provider):

 

    ...

    public CustomFileCacheStorageNode(CustomFileCacheStorageData fileCacheStorageData)

    {

      if (fileCacheStorageData == null)

      {

        throw new ArgumentNullException("CustomFileCacheStorageData");

      }

      Rename(fileCacheStorageData.Name);

      this.pathName = fileCacheStorageData.CacheFilePath;

    }

    ...

 

The remainder of the configuration node class contains the property accessor for each property configurable in the console, and for the property that exposes the populated configuration data to the provider. You use attributes to specify how the Configuration Console will handle and expose each property that the user can configure.

 

The configuration node class for the example provider has to exposes only the CacheFilePath property (the Name and Type properties are exposed by the base class). The attributes for this class (shown in the next listing) indicate that a value for the property is required; and that the editor to present to the user when they edit this property is the SaveFileEditor - a class within the core Enterprise Library configuration that displays a File Save dialog. 

 

The FilteredFileNameEditor attribute indicates the text string from the resources file that the underlying Save File dialog should use as the file type filter (in this example it is "All files (*.*)|*.*"), and the SRCategory and SRDescription attributes specify text strings from the resources file that console will display in the status bar and as help text when configuring this property:

 

    ...

    [Required]

    [Editor(typeof(SaveFileEditor), typeof(UITypeEditor))]

    [FilteredFileNameEditor(typeof(Resources), "FileCachePathFileDialogFilter")]

    [SRCategory("CategoryGeneral", typeof(Resources))]

    [SRDescription("TextAreaNameDescription", typeof(Resources))]

    public string CacheFilePath

    {

      get { return pathName; }

      set { pathName = System.IO.Path.GetDirectoryName(value); }

    }

 

 

The property accessor itself just retrieves and sets the value of the local class-level variable named pathName. However, one minor issue is that the SaveFileEditor class only allows users to select or type a filename, and not just navigate to and select a path to an empty folder. Therefore, the set clause must strip off the filename that the user selects or enters. You may consider cloning the existing SaveFileEditor class and modifying it to allow selection of a folder, or creating your own more suitable descendant of the UITypeEditor base class, if you require this or more specialized functionality in your custom provider(s).

 

Finally, you must override the public CacheStorageData property of the CacheStorageNode base class to expose the current instance of the CustomFileCacheStorageData class that contains the configuration information:

 

    public override CacheStorageData CacheStorageData

    {

      get { return new CustomFileCacheStorageData(Name, pathName); }

    }

  }

}

Editing the Design Resources File

Several values used in the configuration node class you saw in the previous section are text strings extracted from the resource file for the application block. These include the text to display in the status bar and as help text in the Configuration Console, and the Save File dialog filter. There are also some text strings that are used in sections of code you will see in the following sections. Placing these language-specific strings in the project resources file makes future updates easier, and allows you to support multiple languages if required.

 

To add these text strings to the project resources file, open the file Resources.resx from within the Properties section of the Caching.Configuration.Design project. Add the following entries to the file using the resource editor that appears when you open the file:

 

FileCacheNameDescription

Gets or sets the path and name of the cache disk file.

FileCachePathFileDialogFilter

All files (*.*)|*.*

FileCacheUICommandLongText

Add custom file cache storage

FileCacheUICommandText

Custom File Cache Storage

Executing Node and Command Registration

The final stage in enabling design support for your provider is to edit the two classes that register the nodes in the Configuration Console tree view, and add the commands to the main and shortcut menus. The Enterprise Library configuration system allows application blocks to override the Register method in the two classes that register tree view nodes (the NodeMapRegistrar class) and menu commands (the CommandRegistrar class).

 

The Design project for the Caching Application Block contains two classes that extend the NodeMapRegistrar and CommandRegistrar classes. These are named CachingNodeMapRegistrar and CachingCommandRegistrar. You add your own registration code to the Register methods of these classes.

 

For the CachingNodeMapRegistrar class, you simply insert a call to the AddSingleNodeMap method, specifying the text to display in the configuration console for the item, and the types to use for the configuration node and the configuration data:

 

...

AddSingleNodeMap(Resources.FileCacheUICommandText,

                           typeof(CustomFileCacheStorageNode),

                           typeof(CustomFileCacheStorageData));

...

 

For the CachingCommandRegistrar class, you create a routine that calls the AddSingleChildNodeCommand method, specifying the command text for the menus, the text to display in the status bar, the type of the provider configuration node class, the type of the base configuration node class, and the type of the base node class:

 

private void AddFileCacheStorageCommand()

{

  AddSingleChildNodeCommand(Resources.FileCacheUICommandText,

      Resources.FileCacheUICommandLongText, typeof(CustomFileCacheStorageNode),

      typeof(CacheStorageNode), typeof(CacheManagerNode));

}

 

Then you modify the Register method to insert a call to your new routine, followed by a call to the AddDefaultCommands method that specifies the type of the provider configuration node class:

 

...

AddFileCacheStorageCommand();

AddDefaultCommands(typeof(CustomFileCacheStorageNode));

...

Compiling and Deploying the Provider

You have now created all the classes you need, and completed the modifications to existing classes. The next step is to compile the projects and copy the assemblies into the correct folders. The easiest way to achieve this is to use the two utilities available from the Enterprise Library section of your Start menu - Build Enterprise Library and Copy Assemblies to bin Folder.

Configuring the Custom File Cache Provider

Open the Configuration Console from the Enterprise Library section of your Start menu and either create a new application, or open an existing Web.config or App.config file. If you start a new application, or the existing configuration does not already contain the Caching Application Block, right-click on the Application Configuration entry, select New, and click Caching Application Block.

 

Within the Cache Managers section, select the Cache Manager node (or the existing child node that is a renamed cache manager node), right-click, and select New. The shortcut menu shows the new provider type Custom File Cache Storage, which uses the custom provider you added design support to in this article. As you hover over this item with the mouse pointer, the status bar shows the text "Add custom file cache storage" (see Figure 3). You can alternatively select New and Custom File Cache Storage from the Action menu when the Cache Manager node is selected in the tree view.

 

Figure 3 - Selecting the Custom Cache File Storage option that uses the example provider

 

Select the new Custom File Cache Storage item in the tree view and the right-hand window displays the two properties you can edit. You can change the name of this Cache Manager, and you can set the value of the CacheFilePath property.

 

Figure 4 - Changing the name and setting the CacheFilePath property of the example provider

 

Notice that the editor for the FileCachePath property, although it is defined as a String value, displays the (...) button when you select it. Clicking this opens the File Save dialog, where you can navigate to the folder where you want to store the cache files. However, because of the nature of the dialog class (as discussed earlier in the section "Building the Configuration Node Class"), you must either enter a name for a file or select an existing file. And, if you select an existing file, clicking Save prompts you to over-write it. However, once the dialog closes, the property contains just the path because the property accessor code (as you saw in the earlier section) removes the filename.

Using with the Custom File Cache Provider in ASP.NET

In use, the custom cache backing store provider for which you created design support in this article behaves exactly the same at runtime as the non-design configured version you saw in the previous article. This is to be expected, as the only change to the provider is the addition of design support for the Configuration Console. The code that performs the caching operations is identical.

 

Figure 5 shows the Web.config file for the ASP.NET example application open in the Enterprise Library Configuration Console. As well as the Cache Manager that uses the Isolated Storage Backing Store provider, you can see the second Cache Manager that uses the Custom File Backing Store provider described in this article. The CacheFilePath property is set to a temporary folder on the local machine. 

 

Figure 5 - The Caching Application Block configuration for the ASP.NET example application

 

When you select the Custom Disk File Cache option in the ASP.NET example application (see Figure 6), the code uses the CustomFileBackingStore provider to write a DataSet to the cache, and then displays the number of items in the cache.

 

Figure 6 - Adding a DataSet to the cache using the custom caching provider

 

When you click the button to retrieve the DataSet, you see it displayed in the page - just as in the examples in the previous articles in this series (see Figure 7).

 

Figure 7 - Retrieving the DataSet from the cache using the custom caching provider

Summary

This article is the last of a series that look at how you can use Enterprise Library in your ASP.NET applications. The previous articles cover an introduction to Enterprise Library, taking advantages of its features to simplify development of your ASP.NET applications, running your applications and Enterprise Library in partial trust modes, and building custom providers to extend Enterprise Library. This final article completes the process by showing how you can add design support to your custom providers so that they integrate fully with Enterprise Library, becoming indistinguishable from the built-in providers.

 

©2006 Alexander Homer – alex@stonebroom.com