.NET 2 the Max
Master .NET Configuration Files
Master the art of loading and saving configuration files using Visual Studio .NET. Also, learn about planned changes in VS.NET 2005.
Technology Toolbox: VB.NET, C#
You can simplify application deployment and customization greatly by keeping as many configuration settings as possible in a configuration file. Configuration files support XCOPY deployment, and you can edit them easily using any text editor, including Notepad. They can store virtually any kind of information, from simple strings to complex objects. Unfortunately, .NET 1.1 configuration files have a couple of important shortcomings, but I'll show you how to work around these.
Let's begin with the basics. You must name a .NET configuration file after the EXE or DLL assembly it refers to, and you must give it a config extension, such as MyApp.exe.config or mylibrary.dll.config. For example, all ASP.NET configuration files are named web.config. A configuration file can include one or more sections that contain user-defined settings. Odds are that you're familiar with the <appSettings> section, which can contain one or more <add> elements with a key and a value attribute:
<configuration>
<appSettings>
<add key="name" value="Joe Doe" />
</appSettings>
</configuration>
You can create the skeleton of a config file from inside Visual Studio .NET by adding a new item of type Assembly Configuration File to the current project. VS.NET creates this file in the project directory with the name App.config, but copies it to the actual output directory when compiling the project.
VS.NET assumes that all values held in the appSettings section are strings, and you can read them using the ConfigurationSettings type (in the System.Configuration namespace):
Dim settings As NameValueCollection = _
ConfigurationSettings.AppSettings
Dim name As String = settings("name")
If you want to read a value that isn't a string, you must force a conversion to the target type, using one of the language functions such as CInt or CDbl, or one of the methods of the Convert type. If you need to assign the value to the property of a Windows Forms control, you can have VS.NET generate both the configuration file and the code that assigns the property by clicking on the button in the DynamicProperties element in the Properties window and assigning a key to the property (see Figure 1). Visual Studio .NET marks the property whose value you read from the config file with a small cyan icon to the right of its name in the Properties window.
Try Other Config Formats
In some cases, the format of the <appSettings> section isn't exactly what you need for your configuration data. For example, assume you want to parse this configuration file:
<configuration>
<cache duration="60" size="100" />
</configuration>
You need to specify which configuration handler can manage it in order to support this different format. The .NET Framework comes with four different types of configuration handlers, and you can also build your own, if necessary. The .NET handler that can read the <cache> section in this example is the SingleTagSectionHandler type, which you must declare in the <configSections> section (the type attribute is split on multiple lines for typographic reasons only):
<configuration>
<configSections>
<section name="cache"
type="System.Configuration.
SingleTagSectionHandler,
System, Version=1.0.5000.0,
Culture=neutral,
PublicKeyToken=b77a5c561934e089"/>
</configSections>
<cache duration="60" size="100" />
</configuration>
The <section> element can appear in the application configuration file or in the machine.config file. You can use this code to read all the elements in a section handled by a SingleTagSectionHandler object:
Dim values As Hashtable = _
DirectCast(ConfigurationSettings. _
GetConfig("cache"), Hashtable)
duration = CInt(values("duration"))
size = CInt(values("size"))
The GetConfig method returns an object; the type of this object depends on which section handler manages the element you're reading. The SingleTagSectionHandler returns a Hashtable, so you must cast it to a proper variable before you can read the individual attributes in the <cache> section.
The .NET Framework supports three more section handlers. Both NameValueSectionHandler and DictionarySectionHandler support sections with the same format as <appSettings>; you can use them when reading groups of name/value pairs in a section with an arbitrary name. The only difference between these two handlers is that the former returns a NameValueCollection object, whereas the latter returns a Hashtable. This makes the latter more efficient when you have more than one dozen pairs. You can see how to use these handlers in the sample project that accompanies this article (get it here").
All sections must have a matching handler; otherwise, the .NET Framework throws an exception when it parses the configuration file. If you wish to read (and possibly modify) a section in the configuration file by some other meanssuch as through an XmlDocument objectyou must tell the .NET Framework to ignore a given section. You can do this by using the fourth standard section handler, IgnoreSectionHandler. For example, assume you have this configuration file (note: I omitted the complete name of the IgnoreSectionHandler type for brevity):
<configuration>
<configSections>
<section name="comments"
type="System.Configuration.
IgnoreSectionHandler, ..."/>
</configSections>
<comments>
Add your comments here...
</comments>
</configuration>
You can't read the <comments> element using the ConfigurationSettings.GetConfig method, but you can read the element manually using this code:
Dim configFile As String = _
AppDomain.CurrentDomain. _
SetupInformation.ConfigurationFile
Dim xmlDoc As New XmlDocument
xmlDoc.Load(configFile)
Dim xmlElem As XmlElement = _
DirectCast(xmlDoc.SelectSingleNode(_
"//configuration/comments"), _
XmlElement)
MsgBox(xmlElem.InnerText)
Write a Custom Section Handler
Even if you can read a section manually, you should try to use the standard approach based on the ConfigurationSettings class when possible, because it is more efficient. For example, you never need to read the config file more than once if you use the standard approach because the ConfigurationSettings class caches the content of the file automatically and therefore reduces disk activity. Another benefit of the standard approach: You can account for any configuration settings defined in parent configuration files or in machine.config with ease.
Fortunately, writing a custom section handler isn't difficult; it simply boils down to creating a class that implements the IConfigurationSectionHandler interface. This interface exposes only the Create method:
Function Create(parent As Object, _
ByVal configContext As Object, _
ByVal section As XmlNode) As Object
' ...
End Function
The first argument is an object that holds configuration settings (for the section with the same name) read from the parent configuration file. The second argument is an HttpConfigurationContext object that is meaningful only in ASP.NET applications and is Nothing otherwise. The third object is the XML node that contains the section of interest in the configuration file.
Creating a simple handler that can read the text inside an XML element like the <comments> element in the previous configuration file example takes only a few lines of code (see Listing 1). You can use this handler in a configuration file, assuming you define the handler in an assembly named MyLib.dll stored in the same directory as the main application:
<configuration>
<configSections>
<section name="description"
type="MyLib.StringSectionHandler,
MyLib"/>
</configSections>
<description >
Add your description here...
</description >
</configuration>
The companion code for this article comes with a more interesting and useful handler, called EncryptStringSectionHandler. This enhanced handler can read a series of key/value pairs where the value element is encrypted using a symmetric algorithm and rendered as a Base64 string, as in this example (where the encrypted string has been split to fit the line width):
<encrypted>
<add key="username"
value="1sS2q4vi7ljWhFgCcdvG18
BsEv7yjdz/hf/wYY5j1SI=" />
<add key="password"
value="txdPuqrvms0dSLnxbHYrdl
/IZ/mi8q7EC5h2uzi53jM=" />
</encrypted>
The sample project includes a command that generates encrypted values that you can later paste in the configuration file (see Figure 2). Note that a malicious user might disassemble or decompile the EncryptStringSectionHandler class to extract the password used by the encryption algorithm. A more robust mechanism would be to rely on a keyword entered by the user at run time.
Get Ready for .NET 2.0
The support for configuration files in .NET 1.1 lacks two important pieces. First, you can use configuration files only for application-level (global) settings, which excludes user-specific preferences. Second, the ConfigurationSettings class doesn't support writing to configuration files. Actually, these two issues are related because application-level settings should be read-only (and editable only by a system administrator). Only user-level preferences should be writable and unable to affect preferences set by other users.
.NET 2.0 solves both these issues. First, it supports writable user-level settings, which you keep in a file stored in a subdirectory of the c:\Document and Settings\username folder. If you don't want to wait until the next version of the .NET Framework, you can implement your own technique or use a class in the sample application that comes with this article.
Start by defining a class named UserSettingsBase, which exposes two methods that let you load and save data to a configuration file in a user-specific directory (see Listing 2). The code in the Load and Save methods is quite generic and uses reflection to iterate over all the public and private fields of the class. The UserSettingsBase class contains no fields, and you should use it as the base class for a type that contains all the user-specific settings:
Public Class UserPreferences
Inherits UserSettingsBase
Public AutoSave As Boolean = True
Public AutoSaveInterval As Short = 5
End Class
Using the UserPreferences class is easy:
Dim prefs As New UserPreferences
prefs.Load()
' Use preferences as desired
' ...
' Change autosave timeout and save
prefs.AutoSaveInterval = 10
prefs.Save()
The UserSettingsBase class loads and saves only fields of a primitive type (numeric, Boolean, or string). You can implement a preference setting for a nonprimitive type by wrapping a property around a private field:
Private _ForeColor As Integer
Property ForeColor() As Color
Get
Return Color.FromArgb(_ForeColor)
End Get
Set(ByVal Value As Color)
_ForeColor = Value.ToArgb()
End Set
End Property
Be sure to browse the complete source code of the companion project for more information.
About the Author
Francesco Balena has authored several programming books, including Programming Microsoft Visual Basic .NET Version 2003 [Microsoft Press]. He speaks regularly at VSLive! and other conferences, founded the .Net2TheMax family of sites, and is the principal of Code Architects Srl, an Italian software company that offers training and programming tools for .NET developers. Reach him at [email protected].