One of my favorite B-movies is Back to School, starring Rodney Dangerfield. If you are unfamiliar with the movie, Dangerfield’s character Thornton Mellon, a rich clothier for fat people, returns to college. Employing savvy business acumen to get through coursework and exams, he is charged with cheating on finals and is required to take an oral exam. When his love interest and English professor asks him to recite Dylan Thomas’s “Do Not Go Gentle into That Good Night,” Mellon recites the poem. Here is an approximation of the dialogue:
Mellon: Do not go gentle into that good night, old age should burn and rave at close of day; Rage, rage against the dying of the light.
Professor: What does that mean to you?
Mellon: It means I don’t gotta take s**t from nobody.
Through Mellon, the screenwriter interprets the poem to be an affirmation for the dignity and rights of each human soul. We all have power, dignity, and a right to assert our existence in the manner we see fit. (Except for an obvious few malcontents, I am optimistic that most people use this power for good.)
What does this have to do with Visual Basic? Well, it means that you and I each are collaborators on our own terms. That we bring value to every endeavor, have a right to express ourselves, and possess knowledge that engenders a certain power. Read this article and rejoice in your burgeoning power.
This article teaches you how to implement the IConfigurationSectionHandler. Classes implementing this interface permit your code to treat a custom block of XML in a .config file as a static, persisted instance of an object. While you can use the AppSettings collection to read individual .config file settings, section handlers enable you to objectify blocks of XML. The result is that an XML node becomes a strongly typed object from the perspective of your code, rather than a bunch of spurious individual values.
IConfigurationSectionHandler
Interfaces are contracts. Like abstaining from performance-enhancing drugs is becoming an enforced contract for professional athletics, interfaces are participation contracts. Quite simply, if a class wants to participate in a certain defined behavior, the class needs to consent to the terms of the contract. This consent is exemplified by implementing an interface: the contract.
For example, the ConfigurationSettings.AppSettings property is a NameValueCollection. ConfigurationSettings itself participates in the IConfigurationSectionhandler contract by using the predefined NameValueSectionHandler, which implements IConfigurationSectionHandler. The result is that XML tags in <appSettings> that define a name and value pair can be read through the ConfigurationSettings.AppSettings collection.
The <appSettings> section of a .config file is useful for spurious application settings like database connection strings or general application settings, but is less expressive for chunks of related data. One example can be found in Microsoft’s Exception Management Application Block (EMAB—check www.microsoft.com for more information on application blocks). The EMAB permits you to dynamically add assemblies that log exceptions after an application has been deployed. This is useful, for example, when you may want to log exceptions to the Event Log and later add e-mail notification. Rather than rebuilding and redeploying an entire application, you can build just the assembly for e-mail notification, add the configuration section to the .config file, and deploy the e-mailer assembly only.
Clearly, an assembly that has entire behaviors, such as managing mail servers, e-mail addresses, and authentication information is much more advanced than just an application setting like a database string. Such an advanced assembly requires special care and management that a class ideally can handle.
This example strikes out somewhere between the simplicity of reading singular application settings and dynamic application block assemblies. I demonstrate how to objectify the settings one might desire to configure an FTP client. (If you are interested in implementing an FTP client for .NET, check out the knowledgebase article 832679 at www.microsoft.com.)
Creating a Section Handler for FTP Client Settings
Technologies such as XML Web Services and .NET Remoting make it possible to connect new systems to legacy systems, but many legacy systems still do not have Web Service or Remote server-wrappers that expose important behaviors and data. As a result, many companies use FTP clients and servers to move data between new systems and local or remote legacy systems. I have worked on many such projects.
Suppose you are working on an FTP client. Whether automating the migration of legacy data or just building your own FTP tool, a reasonable assumption is that connection settings will benefit from being externalized and editable with a plain text editor.
Defining the XML Section
A good strategy is to define the block of XML you want to read. For the purposes of this example, you might want to turn the FTP behavior on or off and configure the remote host, path, user, password, and port. While the port generally is port 21, supporting the configuration of elements like the port number is still useful. Such an XML section might be added to a .config (see Listing 1).
Listing 1: Defining an ftpSettings section of an XML .config file.
<ftpSettings mode="on"> <remoteHost value="127.0.0.1" /> <remotePath value="." /> <remoteUser value="anonymous" /> <remotePassword value="pkimmel@softconcepts.com" /> <remotePort value="21" /> </ftpSettings>
The XML node is ftpSettings with an attribute named mode. The child XML nodes are remoteHost, remotePath, remoteUser, remotePassword, and remotePort. Each child node has an attribute named value. While XML can be as simple or as complex as you like, the XML section in Listing 1 suits the example’s purposes.
Implementing the Section Handler
The XML node ftpSettings represents an XML-persisted object that you want to be able to read and convert to an instance of a class at runtime. Naturally, you might name such a class FtpSettings and the section handler for FtpSettings FtpSettingsSectionHandler. The FtpSettings class needs fields of the correct type to store the persisted XML data, and IConfigurationSectionHandler requires you to implement a Create method.
Listing 2 shows the implementation of the FtpSettings class and the FtpSettings SectionHandler. I added a few helper details for convenience. A discussion of the Create method follows the listing.
Listing 2: The IConfigurationSectionHandler and FtpSettings classes for managing FTP settings.
Imports System Imports System.Configuration Imports System.Globalization Imports System.Text Imports System.Xml #Region "Sample .config file with configuration section" '<?xml version="1.0" encoding="utf-8" ?> ' <configuration> ' "configSections> ' <section name="ftpSettings" ' type="FtpSettings.FtpSettingsSectionHandler, ' FtpSettings" /> ' </configSections> ' <ftpSettings mode="on"> ' <remoteHost value="127.0.0.1" /> ' <remotePath value="." /> ' <remoteUser value="anonymous" /> ' <remotePassword value="pkimmel@softconcepts.com" /> ' <remotePort value="21" /> ' </ftpSettings> ' </configuration> #End Region Public Enum FtpSettingsMode OFF [ON] End Enum Public Class FtpSettings Public mode As FtpSettingsMode = FtpSettingsMode.ON Public remoteHost As String = "127.0.0.1" Public remotePath As String = "." Public remoteUser As String = "anonymous" Public remotePassword As String = "dummy@nowhere.com" Public remotePort As Integer = 21 Public Function GetFormatted() As String Dim builder As StringBuilder = New StringBuilder builder.AppendFormat("remoteHost={0}{1}", remoteHost, _ Environment.NewLine) _ builder.AppendFormat("remotePath={0}{1}", remotePath, _ Environment.NewLine) builder.AppendFormat("remoteUser={0}{1}", remoteUser, _ Environment.NewLine) builder.AppendFormat("remotePassword={0}{1}", remotePassword, _ Environment.NewLine) builder.AppendFormat("remotePort={0}{1}", remotePort, _ Environment.NewLine) Return builder.ToString() End Function End Class Public Class FtpSettingsSectionHandler Implements IConfigurationSectionHandler Private Const FTPSETTINGS_MODE As String = "mode" Private Const REMOTE_HOST As String = "remoteHost" Private Const REMOTE_PATH As String = "remotePath" Private Const REMOTE_USER As String = "remoteUser" Private Const REMOTE_PASSWORD As String = "remotePassword" Private Const REMOTE_PORT As String = "remotePort" Public Sub New() End Sub Public Function Create(ByVal parent As Object, _ ByVal configContext As Object, _ ByVal section As System.Xml.XmlNode) As Object _ Implements IConfigurationSectionHandler.Create Try Dim settings As FtpSettings = New FtpSettings If (section Is Nothing) Then Return settings Dim currentAttribute As XmlNode Dim nodeAttributes As XmlAttributeCollection = _ section.Attributes currentAttribute = _ nodeAttributes.RemoveNamedItem(FTPSETTINGS_MODE) If (Not currentAttribute Is Nothing And _ currentAttribute.Value.ToUpper(CultureInfo.InvariantCulture) = _ "OFF") Then settings.mode = FtpSettingsMode.OFF End If Dim node As XmlNode For Each node In section.ChildNodes nodeAttributes = node.Attributes If nodeAttributes Is Nothing Then GoTo Continue currentAttribute = nodeAttributes.RemoveNamedItem("value") If (currentAttribute Is Nothing) Then GoTo continue If (node.Name = REMOTE_HOST) Then settings.remoteHost = currentAttribute.Value End If If (node.Name = REMOTE_PATH) Then settings.remotePath = currentAttribute.Value End If If (node.Name = REMOTE_USER) Then settings.remoteUser = currentAttribute.Value End If If (node.Name = REMOTE_PASSWORD) Then settings.remotePassword = currentAttribute.Value End If If (node.Name = REMOTE_PORT) Then settings.remotePort = _ Convert.ToInt32(currentAttribute.Value) End If ' A little short-circuiting never killed anyone Continue: Next Return settings Catch ex As Exception Throw New ConfigurationException("Error loading FtpSettings", _ ex, section) End Try End Function End Class
The areas of interest are in boldface. The first is the FtpSettings class. Create returns an object that is the base type for all .NET classes; thus, the IConfigurationSectionHandler.Create method can be used for any type. The FtpSettings class itself is little more than a data structure, and the GetFormatted method is really for debugging purposes only.
The FtpSettingsSectionHandler uses the implements keyword to indicate that a contract exists between this class and the IConfigurationSectionHandler interface. The Create method satisfies the contractual requirement.
The static method ConfigurationSettings.GetConfig called with an “ftpSettings” argument would implicitly invoke the Create method. Passed to Create are three arguments:
- Settings in a corresponding parent section
- An instance of HttpConfigurationContext or reserved
- XmlNode representing our custom section
Ultimately, what you want to do is create an instance of your target type, read each node and child node, and populate the fields of your target type, FtpSettings.
The first statement in Create instantiates an instance of your target type, FtpSettings. If the argument section is null, the caller gets a default instance of the objectified section. Because you have default configuration settings, this might be suitable. It at least ensures that callers get an instance of the FtpSettings rather than null.
Next, you need to read attributes of each node, child nodes and their attributes, and so on, recursively. Your <ftpSettings> section is nested only one child deep, so that is all you will read. Anything not specifically looked for in your section handler will be ignored. The first attribute you read is the mode. This is useful if you wanted to enable or disable an FTP process post-deployment. After you read the parent node, you need to read each child node. As demonstrated by the code, you literally examine the node name; if the node name matches a name of interest, you read that node’s “value” attribute.
Note: Don’t let the GOTO statement bother you. Decades ago, GOTO (branch) statements were all the rage. Then, enlightened scholars decided that arbitrarily located branch statements were a blight on comprehensible code. The truth, as is with all things, lies somewhere in between. Ultimately, code is converted to machine language, which is riddled with branch statements (je, jne, jmp, and many more). If used with care, and perhaps sparingly, there is nothing wrong with an occasional GOTO statement. However, GOTO is not a carte blanche device intended to support spaghetti code.
Because you know the expected type of each node, you can perform type conversions and handle invalid types. For example, the remote port is typecast to an integer type. In the catch block, you convert any generic exception to a ConfigurationException and alert the caller about a problem in the .config file. Assuming no exception occurs, you return the fully instantiated FtpSettings object at the end of the try block.
Reading the Persisted Object
To read the object represented by a custom configuration section, you need to call ConfigurationSettings.GetConfig and pass the case-sensitive name of the XML section. The return type is an object type. In Visual Basic, you can assign an arbitrary object to a specific type, but it is preferable to typecast the generic object type to the specific type represented by the configuration section. Listing 3 shows the weakly typed implicit conversion supported by Visual Basic .NET first, followed by the more explicit example required by non-VB programmers.
Listing 3: Reading and instantiating persisted configuration objects.
Imports FtpHelpers Imports System.Configuration Module Module1 Sub Main() Dim settings As FtpSettings = _ ConfigurationSettings.GetConfig("ftpSettings") Console.WriteLine(settings.GetFormatted()) Console.ReadLine() Dim settings2 As FtpSettings = _ CType(ConfigurationSettings.GetConfig( _ "ftpSettings"), FtpSettings) Console.WriteLine(settings.GetFormatted()) Console.ReadLine() End Sub End Module
Finally, if your project or module is configured with Option Strict On, the first chunk of code will not work. Implicit type conversion does not work in strict mode.
A Judgment Call
Beginner computer programmers are given a bunch of rules: Do this, but don’t do that. As one’s skill increases, an understanding of the origin of the rules grows. Finally, one learns to bend—and even break—the rules, and understands when doing so is permissible. A good example is the GOTO statement. This is called judgment.
Judgment is that quality that helps you decide when existing code is sufficient and when you have to write custom code. If you are not sure of your own judgment, use the AppSettings property to read externalized values. If you have a reason for doing so, write your own .config section handler. This article has shown you how.
The problem is judgment is subjective. If your judgment leads you to roll your own section handler, which takes longer and interferes with your schedule, your judgment, while technically superior, may be called into question. Existing code is often quicker and less likely to induce bugs in the short term—and the short term is where many business people and managers dwell.
Biography
Paul Kimmel is the VB Today columnist, a Visual Developer MVP, the president of Lansing, Michigan’s .NET Users Group, and has written several books on object-oriented programming. Paul is the chief architect for Software Conceptions, Inc., founded in 1990. You may contact him at pkimmel@softconcepts.com if you need assistance developing software or are interested in joining, presenting at, or sponsoring the Lansing Area .NET Users Group (www.glugnet.org).
Copyright © 2004 by Paul Kimmel. All Rights Reserved.