Configure This
Parameterize Your Apps Using XML Configuration In The .NET Framework 2.0
Bryan Porter
This article discusses:
- Configuration with the .NET Framework 1.x
- Configuration with the .NET Framework 2.0
- Implementing custom configuration sections, collections,
elements, and groups
- Creating custom edit controls
|
This article uses the following technologies:
Visual Basic, .NET Framework 2.0
|
Code download available at:
Configuration2006_06.exe
(187 KB)
Browse the Code Online

Contents
A
pplications are complicated. Even the simplest software invariably contains numerous settings that have to store some sort of value in order to function at even a basic level. Database-driven applications usually have the luxury of storing the lion's share of their configuration information in a database of some sort, but there are always settings and options whose values can't be stored there (such as the location of the database itself) or are inconsequential to system reliability but absolutely necessary to preserve the user experience.
I've done my fair share of work on projects as a "clean-up" man, where the functional specifications had all been met, but there was a growing dissatisfaction on the part of the end users. Management couldn't understand what the problem was—the application did all that had been requested! Why weren't the users satisfied? After doing some end-user interviews, I discovered that most of the complaints concerned all the little ancillary things—like windows not opening at the same location on the screen each time, or button and menu shortcuts being inconsistent. All these little things the original engineers hadn't thought about gave rise to unhappy users who felt that the app was broken. In fact, it wasn't broken at all; it was just poorly configured.
Configuration in the Microsoft® .NET Framework 1.x was a great boon for developers. The structured XML configuration file was easy to work with from a developmental standpoint, and it freed developers using .NET from having to store configuration information in the Windows® Registry. Using the XML configuration file, xcopy deployment was finally a reasonable option for many applications. That's not to say that the configuration system in the .NET Framework 1.x was without its own set of problems. Lack of an API for editing the configuration file and the unstructured nature of custom configuration sections all gave rise to inconsistencies in application configuration and management.
Thankfully, those idiosyncrasies have been addressed in the .NET Framework 2.0. A new set of classes now give you read/write access to your application's configuration file and allow you to house structured, strongly typed and validated configuration data. For any developer who spent much time with .NET Framework 1.x configuration files and IConfigurationSectionHandler, this is a welcome change.
In this article, I'll discuss different ways to configure an application in the .NET Framework 2.0, exploring the classes of the newly revamped System.Configuration namespace. Along the way I'll show you how, with just a little bit of planning, you can construct a dialog box to automatically manage the configuration settings of your application.
The Basics
Deciding what configuration information to store, and how to store it, is a big step in determining the best way to architect configuration for your application. Take the case of storing the majority of user-specific configuration data in the database of a database-driven application. This can be a good decision for some usage scenarios, since any change users might make to configuration values will follow them around to any machines on which they run that application. The flip side, of course, is that you've now got a ton of extra information in your database that isn't particularly pertinent to the operation of your application.
Alternatively, you can use the XML configuration file. Visual Studio® 2005 introduces support for simplifying this process, complete with an auto-generated object model to make accessing simple configuration values a snap. If you double-click on the My Project folder in Solutions Explorer (Properties for a C# project), you'll open the properties document for your project. Select the Settings tab and choose User as the configuration scope for any settings you want to add. Once you've added User-scoped settings to your project, you can access these settings through the My.Settings property (Properties.Settings.Default in C#) at run time. After creating a few different settings, open the app.config file in Visual Studio, and you'll see something similar to Figure 1.

Figure 1 App.config User Settings
<configuration>
<configSections>
<sectionGroup name="userSettings"
type="System.Configuration.UserSettingsGroup, System,
Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089">
<section name="MSDNSampleSettings.My.MySettings"
type="System.Configuration.ClientSettingsSection, System,
Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
allowExeDefinition="MachineToLocalUser"
requirePermission="false" />
</sectionGroup>
</configSections>
...
<userSettings>
<MSDNSampleSettings.My.MySettings>
<setting name="Setting" serializeAs="String">
<value>SomeDefaultValue</value>
</setting>
</MSDNSampleSettings.My.MySettings>
</userSettings>
</configuration>
The exact XML will be different depending on what settings you've specified, but the general structure will be the same. The great thing about the settings designer in Visual Studio 2005 is that all the features of the configuration system that it takes advantage of to produce the configuration file can be used by the developer to do the same. This allows you to define configuration elements structured just like the .NET Framework assemblies. In fact, if you open the machine.config file for your workstation, you'll see a very similar structure.
The Visual Studio designer uses implementations of ConfigurationSection and ConfigurationSectionGroup to perform this magic. While the built-in Visual Studio designer can handle many of your configuration needs, eventually you may run into a situation where your configuration data is a little more complicated than a simple name-value combination. It's in these situations that the new .NET Framework configuration model really shines.
A configuration section, like the MSDNSampleSettings.My.MySettings section in Figure 1, is created by defining a class that inherits from ConfigurationSection, defined in the System.Configuration assembly. (New for the .NET Framework 2.0, the System.Configuration namespace now spans both the System and System.Configuration assemblies.) The attributes and their corresponding values for a given configuration section element are mapped to the properties of a ConfigurationSection implementation through the ConfigurationProperty attribute. The code in Figure 2 shows a sample implementation of a custom configuration section.

Figure 2 ConfigurationSection
Imports System.Configuration
Imports System.ComponentModel
Public Class CustomConfigurationSection
Inherits System.Configuration.ConfigurationSection
Public Sub New()
MyBase.New()
End Sub
<ConfigurationProperty("testString", _
IsRequired:=True, IsKey:=False, _
DefaultValue:="AStringMoreThanTenLong"), _
StringValidator(InvalidCharacters:="xyz", _
MaxLength:=50, MinLength:=10)> _
Public Property TestString() As String
Get
Return CType(Me("testString"), String)
End Get
Set(ByVal value As String)
Me("testString") = value
End Set
End Property
<ConfigurationProperty("testInt", IsRequired:=True, IsKey:=False, _
DefaultValue:=10), _
IntegerValidator(ExcludeRange:=False, _
MaxValue:=500, MinValue:=10)>
Public Property TestNumber() As Integer
Get
Return CType(Me("testInt"), Integer)
End Get
Set(ByVal value As Integer)
Me("testInt") = value
End Set
End Property
<ConfigurationProperty("testBool", IsRequired:=True, IsKey:=False, _
DefaultValue:=False)> _
Public Property TestBool() As Boolean
Get
Return CType(Me("testBool"), Boolean)
End Get
Set(ByVal value As Boolean)
Me("testBool") = value
End Set
End Property
End Class
Pay special notice to the application of the ConfigurationProperty attribute as shown in the following:
<ConfigurationProperty("testString", IsRequired:=True, IsKey:=False, _
DefaultValue:="AStringMoreThanTenLong"), _
StringValidator(InvalidCharacters:="xyz", _
MaxLength:=50, MinLength:=10)> _
Public Property TestString() As String
The ConfigurationProperty attribute takes a number of different arguments in its constructor, and the one you should be most interested in is the first argument, which defines the corresponding attribute name for this custom implementation's corresponding XML node. The name property maps your class's property to the property name in an internal property collection provided by the base class. My class simply takes whatever value is passed and inserts it into this internal property collection. The .NET Framework maps class property metadata to corresponding XML attribute properties in the app.config file. Additionally, the implementation here uses a ConfigurationValidator to specify that the value of this property cannot contain characters x, y, or z, must be no longer than 50 characters, and must be at least 10 characters long. I'll talk a bit more about ConfigurationValidators later on.
An example of an XML element that corresponds to this custom configuration section implementation in
Figure 2 is:
<sampleCustomConfig testString="testtesttesttest" testInt="250"
testBool="false" />
If you were to type the code from
Figure 2 into your Visual Studio project and add the XML fragment just shown to your app.config, you'd quickly find your application has no access to the defined element. Even though you've constructed a perfectly valid ConfigurationSection implementation, you haven't yet instructed the .NET Framework configuration system to use this new configuration information. Because of this, the .NET Framework has no idea what the sampleCustomConfig element means.
For the .NET Framework configuration system to make use of your custom configuration implementation, you must first define it in the <configSections> element of your app.config file. For instance:
<configSections>
<section name="sampleCustomConfig"
type="MSDN.Configuration.Demo.CustomConfigurationSection,
MSDN.Configuration.Demo" />
</configSections>
This tells the .NET Framework configuration system that while parsing your app.config file, whenever it encounters an XML node named sampleCustomConfig, it should map the attributes and values of that element according to the rules specified by the property attributes on the CustomConfigurationSection class.
Referring back to the code in Figure 2, notice that some of the properties I've defined and marked with a ConfigurationProperty attribute have a *Validator attribute applied as well. These attributes allow you to validate the data that gets populated in the properties of your class. A number of configuration validators ship with the .NET Framework, such as ones for validating based on regular expressions, strings, integers, and more. For a full list, see the MSDN® documentation on ConfigurationValidatorBase, ConfigurationValidatorAttribute, and the types that derive from each.
While the mapping and validation might seem straightforward, there is a lot more happening behind the scenes. Exactly how does the .NET Framework convert the string representation of your configuration value to the correct type for storage in your ConfigurationSection derived class? Additionally, is this conversion mechanism only available for standard .NET types or could you use your own types? For instance, take the TestNumber property used in the sample. This property gets or sets an Integer value, but the XML config file stores that integer as plain text, between quotes. How is this conversion performed automatically?
There are actually two answers to these questions: IConvertible and TypeConverter. IConvertible defines a number of methods for converting a type between the different standard .NET types, and is implemented by the standard .NET value types (Int32, String, and so on). Alternatively, an implementation of a custom TypeConverter for a particular class would allow you to convert a string representation of a value to an entirely custom value.
Suppose I wanted to return a type from one of my configuration section's property classes that had no TypeConverter defined for converting itself to a string representation. At run time, I'd get an exception since the .NET Framework wouldn't know how to do the conversion. In that instance, I'd have to define a TypeConverter. Defining TypeConverters is beyond the scope of this article, but for more information, see the MSDN documentation regarding the IConvertible interface, the TypeDescriptor class, and the TypeConverter class and its associated attribute.
Now, configuration sections standing on their own are plenty useful, but for organizational purposes you might want to group them together. Enter ConfigurationSectionGroup—this class allows you to define a grouping tag to keep your configuration elements ordered logically.
To specify a grouping tag to be used by your configuration section, modify the <configSections> entry in your app.config file as shown in the following snippet:
<configSections>
<sectionGroup name="sampleSectionGroup">
<section name="sampleCustomConfig"
type="MSDN.Configuration.Demo.CustomConfigurationSection,
MSDN.Configuration.Demo" />
</sectionGroup>
</configSections>
Then, modify your app.config file to nest the <sampleCustomConfig> element defined earlier to appear inside a matching set of <sampleSectionGroup> tags:
<sampleSectionGroup>
<sampleCustomConfig testString="testtesttesttest"
testInt="250"
testBool="false" />
</sectionGroup>
You can leave off the type specification for a section group in your <configSections> tag, and the .NET Framework will use the default implementation of ConfigurationSectionGroup (which is suitable for most purposes). But I recommend that instead of relying on this default behavior, you go ahead and define a custom ConfigurationSectionGroup-derived class, even if you don't provide any additional functionality in it. By so doing, you can easily augment the base functionality of the ConfigurationSectionGroup class in the future and provide additional details as to the contents of your ConfigurationSectionGroup (something I will take advantage of shortly).
An implementation of a custom configuration section group is exceedingly trivial. Simply define a class that inherits from ConfigurationSectionGroup and whose constructor calls down to the base class constructor. Once that's done, the associated entry in the <configSections> tag looks like this:
<configSections>
<sectionGroup name="sampleSectionGroup"
type="MSDN.Configuration.Demo.CustomConfigurationSectionGroup,
MSDN.Configuration.Demo">
...
</sectionGroup>
</configSections>
Collections
There is one more facet of the configuration system I've yet to discuss: collections. Frequently, you'll encounter configuration sections that use <add>, <remove> or <clear/> elements in the XML element body. Internally, these elements are actually populating a ConfigurationElementCollection property on the ConfigurationSection. Defining one is relatively easy, using skills I've already discussed. Simply derive two new classes, one each from ConfigurationElement and ConfigurationElementCollection. Your ConfigurationElement-derived class will become the class that's instantiated for each <add> element in your XML file. The ConfigurationElementCollection class has two key methods you'll need to override: CreateNewElement and GetElementKey.
When you are defining your properties in the ConfigurationElement-derived class, it's important to specify at least one property as that element's key. Without this specification, the configuration system won't be able to sort the elements in your ConfigurationElementCollection-derived class. Figure 3 contains the code detailing a sample implementation of ConfigurationElement and ConfigurationElementCollection.

Figure 3 Defining a Collection
CustomConfigurationElement.vb
Imports System.Configuration
Imports System.ComponentModel
Public Class CustomConfigurationElement
Inherits System.Configuration.ConfigurationElement
<ConfigurationProperty("testString", DefaultValue:="", IsKey:=True, _
IsRequired:=True) > _
Public Property TestElementString() As String
Get
Return Me("testString").ToString()
End Get
Set(ByVal value As String)
Me("testString") = value
End Set
End Property
End Class
CustomConfigurationElementCollection.vb
Imports System.Configuration
Public Class CustomConfigurationElementCollection
Inherits System.Configuration.ConfigurationElementCollection
Protected Overloads Overrides Function CreateNewElement() _
As ConfigurationElement
Dim configurationElement As New CustomConfigurationElement()
Return configurationElement
End Function
Protected Overrides Function GetElementKey( _
ByVal element As ConfigurationElement) As Object
If (TypeOf (element) Is CustomConfigurationElement) Then
Dim configElement As CustomConfigurationElement = _
CType(element, CustomConfigurationElement)
Return configElement.TestElementString
End If
Throw New ArgumentException( _
"The specified element is not of the correct type.")
End Function
End Class
To add this collection to the previously defined CustomConfigurationSection class, add the following to the code shown in
Figure 2:
<ConfigurationProperty("namedCollection", IsDefaultCollection:=False, _
IsRequired:=False)> _
Public ReadOnly Property ElementCollection() As _
CustomConfigurationElementCollection
Get
Return CType(Me("namedCollection"), _
CustomConfigurationElementCollection)
End Get
End Property
Alternatively, you can replace the namedCollection property name with empty quotes and set IsDefaultCollection equal to true; this has the effect of establishing a default collection for the ConfigurationSection, allowing you to write XML similar to the following snippet:
<sampleSectionGroup>
<sampleCustomConfig testString="testtesttesttest" testInt="250">
<add testString="someString" />
<add testString="someOtherString" />
</sampleCustomConfig>
</sampleSectionGroup>
To get a better understanding of the inheritance relationship of these custom configuration classes and their .NET Framework base classes, take a look at
Figure 4. Notice that ConfigurationSection and ConfigurationElementCollection share a common base class, ConfigurationElement. You can take advantage of this inheritance hierarchy when constructing ConfigurationElementCollection and ConfigurationElement editor controls later on.
Figure 4 Custom Configuration Classes and .NET Framework Base Classes
I haven't yet discussed how to actually access the configuration information stored using our new, structured ConfigurationSection derived classes. Information stored in the configuration file is accessed through the Configuration class, which is returned by the ConfigurationManager class and its OpenExeConfiguration method. The OpenExeConfiguration method takes a value from the ConfigurationUserLevel enumeration as an argument, to indicate which of the configuration files you actually want to open: the configuration file that applies to all users, the configuration file only applying to roaming users for the current user, or the configuration that applies to the current user. For more information on the OpenExeConfiguration method, and the ConfigurationManager class in general, see the MSDN documentation.
Once you've obtained a reference to the current application's Configuration object, you can access any sections not nested inside ConfigurationSectionGroup through the Configuration object's Sections property. Additionally, there is a SectionGroups property on the Configuration class that returns a collection of the defined ConfigurationSectionGroups. You can access both of these by either name or ordinal:
Dim mySection as CustomConfigurationSection = _
CType(config.Sections("sampleCustomConfig"), _
CustomConfigurationSection)
...
Dim mySectionGroup as CustomConfigurationSectionGroup = _
CType(config.SectionGroups("sampleSectionGroup"), _
CustomConfigurationSectionGroup)
One thing you are sure to notice when poking around the Configuration object for your application is that there are a lot of sections and section groups returned by the object that aren't in your app.config file. These values are actually contained in a parent configuration file. For the examples shown here, your configuration file inherits numerous default values from the system's machine.config file.
Putting It to Use
Having a well-structured configuration system is all well and good, but do you really want your users wading through XML in order to change a setting in your application? Absolutely not! While you could certainly hardcode a dialog that could manipulate your custom ConfigurationSection-derived classes, this would be tedious and potentially error-prone. Instead, I'll show you how to build a Windows Forms dialog, similar to the one shown in Figure 5, that can handle modifying the configuration for you.
Figure 5 Editing the Custom Configuration
Start out by creating a new Windows Forms form and place a SplitContainer on it. In the left-hand pane of the split container, drop a TreeView control for a normal, Explorer-like set up. Below the SplitContainer, add two buttons, OK and Cancel, and drop a Checkbox control aligned with the buttons on the left-hand side of the form. The TreeView control will show the "outline" of your configuration file, and you can use the TreeNode's tag property to house the individual ConfigurationSection, ConfigurationSectionGroup, and ConfigurationElementCollections your dialog will be set up to edit. The Checkbox control will be used to enable and disable the display of system-defined configuration sections.
There are five methods that need closer attention: RefreshTree, GetSectionNodes, GetSectionGroupNodes, ShouldIncludeSection, and ShouldIncludeSectionGroup (see Figure 6). GetSectionNodes and GetSectionGroupNodes recurse through the hierarchy of ConfigurationSections and ConfigurationSectionGroups accessible through the Configuration class. Since a ConfigurationSectionGroup can hold nested ConfigurationSectionGroups, you have to handle any recursive scenarios that might arise. The GetSectionNodes and GetSectionGroupNodes methods both return an array of TreeNodes for easy insertion into the TreeView.

Figure 6 Building the Tree Structure of the Configuration File
Private Sub RefreshTree()
treeSections.Nodes.Clear()
Dim systemNode As New TreeNode("System Configuration")
systemNode.Nodes.AddRange(GetSectionNodes())
treeSections.Nodes.AddRange(GetSectionGroupNodes(Nothing))
If (systemNode.Nodes.Count > 0) Then
treeSections.Nodes.Add(systemNode)
End If
End Sub
Private Function GetSectionNodes(ByRef config As _
System.Configuration.Configuration) As TreeNode()
Dim nodes As New List(Of TreeNode)
For Each section As ConfigurationSection In config.Sections
If (ShouldIncludeSection(section)) Then
Dim sectionNode As New TreeNode(GetSectionName(section))
sectionNode.Tag = section
BuildSectionNode(section, sectionNode)
nodes.Add(sectionNode)
End If
Next
Return nodes.ToArray()
End Function
Private Function GetSectionGroupNodes(ByRef group _
As ConfigurationSectionGroup) As TreeNode()
If (group Is Nothing) Then
Dim nodes As New List(Of TreeNode)
For Each childGroup As ConfigurationSectionGroup _
In config.SectionGroups
If (ShouldIncludeSectionGroup(childGroup)) Then
nodes.AddRange(GetSectionGroupNodes(childGroup))
End If
Next
Return nodes.ToArray()
Else
Dim node As New TreeNode(GetSectionGroupName(group))
For Each childGroup As ConfigurationSectionGroup _
In group.SectionGroups
If (ShouldIncludeSectionGroup(childGroup)) Then
node.Nodes.AddRange(GetSectionGroupNodes(childGroup))
End If
Next
For Each section As ConfigurationSection In group.Sections
If (ShouldIncludeSection(section)) Then
Dim childNode As New TreeNode(GetSectionName(section))
childNode.Tag = section
BuildSectionNode(section, childNode)
node.Nodes.Add(childNode)
End If
Next
Return New TreeNode() {node}
End If
End Function
Private Function ShouldIncludeSection(ByRef section As _
ConfigurationSection) As Boolean
Dim sectionType As Type = section.GetType()
If (hideSystemConfig.Checked And _
sectionType.Assembly.FullName.StartsWith(«System»)) Then
Return False
End If
Return True
End Function
Private Function ShouldIncludeSectionGroup(ByRef sectionGroup As _
ConfigurationSectionGroup) As Boolean
Dim groupType As Type = sectionGroup.GetType()
If (hideSystemConfig.Checked _
And groupType.Assembly.FullName.StartsWith(«System») _
And Not groupType.FullName.Contains(_
«.ConfigurationSectionGroup»)) Then
Return False
End If
Return True
End Function
The RefreshTree method simply starts the whole process by first ensuring that the TreeView is cleared and constructing a parent node to handle ConfigurationSections that aren't parented by a ConfigurationSectionGroup. The ShouldIncludeSection and ShouldIncludeSectionGroup contain some filtering logic to separate out ConfigurationSections and ConfigurationSectionGroups that are defined in machine.config. Use these in combination with the checkbox control to optionally display or hide system-defined configuration elements.
Since sections can appear outside of section groups and section groups can be nested, you need two different methods to enumerate sections and section groups. When GetSectionGroupNodes is called the first time (from RefreshTree), Nothing (null in C#) is passed as the argument. The method checks this argument, and if it's Nothing, you enumerate through the root section groups returned by the Configuration objects SectionGroups property.
You then call GetSectionGroupNodes again for each of the ConfigurationSectionGroups returned. Since successive calls to GetSectionGroupNodes pass a reference to a ConfigurationSectionGroup object (instead of Nothing) as the method argument, a second code path is executed that enumerates the ConfigurationSections and ConfigurationSectionGroups of the passed ConfigurationSectionGroup object. A TreeNode is constructed for each ConfigurationSectionGroup and ConfigurationSection reference found inside the passed ConfigurationSectionGroup object, and each tree node has its corresponding ConfigurationSectionGroup or ConfigurationSection reference stored in the nodes Tag property. This way, when you wire up events to handle TreeNode selection, you immediately have access to the selected nodes associated ConfigurationSection or ConfigurationSectionGroup.
Filtering out sections defined by the framework is as simple as calling RefreshTree from within the CheckedChanged event on the forms checkbox. For production applications, you'll probably want to filter out system-defined configuration sections at all times, but during development it might be handy to enable that functionality, if only to see what else you've got defined in your app.config file.
One problem with enumerating the different sections and section groups in this manner is one of usability: the names of the different sections or section groups make sense in the context of your XML configuration file, but most users won't understand what they mean. You'll need to provide a way to add descriptive information to the ConfigurationSection and ConfigurationSectionGroup-derived classes. Luckily, the .NET Framework has done much of the work already.
The Description attribute class resides in the System.ComponentModel namespace and is useful for attaching simple descriptive strings to class elements. By modifying your custom configuration section and applying the Description attribute, you can provide a descriptive name for your configuration sections and groups that are a little more user friendly than the values provided by SectionName property of the ConfigurationSections or the SectionGroupName property of the ConfigurationSectionGroups. The resulting class declaration now looks like this:
<Description("Custom Configuration Section")> _
Public Class CustomConfigurationSection
Inherits System.Configuration.ConfigurationSection
At run time, you can now read the metadata of the class definition to provide a superior user experience. This same problem occurs with the properties defined in the custom configuration section, but by applying the Description attribute to those properties, you can present a nicely descriptive user interface to the end user. What's more, the PropertyInformation objects (describing the configuration properties handled by your ConfigurationSection-derived class) retrieved from a configuration section's ElementInformation.Properties collection, will populate their Description property with whatever text you specify in the Description attribute applied to a particular property in the class definition. For instance, you can access the properties description attribute that has been defined in this manner:
<ConfigurationProperty("testString", IsRequired:=True, _
IsKey:=False, DefaultValue:="AStringMoreThanTenLong"), _
StringValidator(InvalidCharacters:="xyz", MaxLength:=50, _
MinLength:=10), _
Description("A Test String")> _
Public Property TestString() As String
To access the properties, you'd use code as simple as this (assuming mySection is a reference to your ConfigurationSection):
mySection.ElementInformation.Properties("testProp").Description
The rest of the configuration system doesn't display PropertyInformation's penchant for Description attributes, so I wrote up a few different private helper methods to grab the Description attribute for sections or section groups.
Figure 7 contains the code for three methods, GetSectionName, GetSectionGroupName, and GetFriendlyName. GetSectionName and GetSectionGroupName call GetFriendlyName, passing in a reference to the Type object that defines the ConfigurationSection or ConfigurationSectionGroup. GetFriendlyName then inspects the passed Type object for any Description attributes that might be applied to it and returns the string contained in the first Description attribute it encounters, or returns Nothing if it found none.

Figure 7 Getting Friendly Names
Private Function GetSectionName(_
ByRef section As ConfigurationSection) As String
Dim sectionName As String = Nothing
sectionName = GetFriendlyName(section.GetType())
If (sectionName IsNot Nothing) Then
Return sectionName
Else
Return section.SectionInformation.SectionName
End If
End Function
Private Function GetSectionGroupName(ByRef group As _
ConfigurationSectionGroup) As String
Dim groupName As String = Nothing
groupName = GetFriendlyName(group.GetType())
If (groupName IsNot Nothing) Then
Return groupName
Else
Return group.SectionGroupName
End If
End Function
Private Function GetFriendlyName(ByRef classType As Type) As String
Dim attributes As System.ComponentModel.DescriptionAttribute()
attributes = classType.GetCustomAttributes( _
GetType(System.ComponentModel.DescriptionAttribute), True)
If (attributes.Length > 0) Then
Return attributes(0).Description
End If
Return Nothing
End Function
If GetFriendlyName returned Nothing, GetSectionName and GetSectionGroupName both return the values found in the SectionName or SectionGroupName, respectively. Otherwise, GetSectionName and GetSectionGroupName just return the value returned by GetFriendlyName. I use these when I'm populating the TreeView with the configuration outline.
But back to the task at hand, for each ConfigurationSection found in the configuration object, you call a private method named BuildSectionNode with the ConfigurationSection you're currently working with and the ConfigurationSection's corresponding TreeNode as arguments. BuildSectionNode inspects the configuration properties of the ConfigurationSection, looking for any properties that return a ConfigurationElementCollection-derived type. If one is found, it creates another node beneath the passed node with either the name/description of the collection property or Values (it needs some sort of name) if the property returns a default collection. You then set this new child node's Tag property to the ConfigurationElementCollection reference.
Now that you've parsed the configuration objects and populated the tree, its time to create edit controls.
Creating the Edit Controls
In creating the edit controls, I had two major goals: to provide validation by respecting the ConfigurationValidator attributes applied to the exposed properties of the ConfigurationElements and ConfigurationSections, and to provide collection editing support. The first item is handled by a custom control derived from Panel, the second by a UserControl.
Editing an item is fairly straightforward. The general idea is to take a ConfigurationElement, enumerate the properties, and for each property create a label and corresponding textbox, storing the PropertyInformation reference that a given textbox is meant to edit in that textbox's Tag property. Then, wire up to the Enter, Leave, TextChanged, KeyUp, and Validating events of the textbox. During the validation event, manually fire the associated property validator. If it fails, raise an error on the textbox with a standard .NET error provider, setting the error text to be the message contained in the exception thrown by the property validator. The Enter, Leave, TextChanged and KeyUp events handle rolling back an edit made if the user presses the escape key while editing.
Laying out the different labels and textboxes before the release of the .NET Framework 2.0 was a bit of a challenge. Thankfully, the .NET Framework 2.0 introduces a number of new layout containers that ease this burden. For the purposes of the ConfigurationElement editor, I decided on the TableLayoutPanel, which lets you arrange controls in rows and columns, similar to HTML tables, and is most handy for making sure labels are appropriately aligned with the textboxes. Figure 8 shows the code used to set up this control.

Figure 8 Setting Up the Element Editor
Private Sub ProcessElement(ByRef element As ConfigurationElement, _
ByRef panel As TableLayoutPanel)
Dim propertyLabel As Label = Nothing
Dim propertyValue As TextBox = Nothing
For Each propInfo As PropertyInformation In _
element.ElementInformation.Properties
If (propInfo.Type.IsSubclassOf( _
GetType(ConfigurationElementCollection))) Then
Continue For
ElseIf (propInfo.Type.IsSubclassOf( _
GetType(ConfigurationElement))) Then
ProcessElement(CType(propInfo.Value, _
ConfigurationElement), panel)
Return
End If
propertyLabel = New Label()
propertyValue = New TextBox()
If (propInfo.Description <> String.Empty) Then
propertyLabel.Text = propInfo.Description
Else
propertyLabel.Text = propInfo.Name
End If
propertyLabel.AutoSize = True
propertyLabel.TextAlign = Drawing.ContentAlignment.MiddleLeft
propertyValue.Anchor = AnchorStyles.Left Or _
AnchorStyles.Right Or _
AnchorStyles.Top
If (propInfo.Value IsNot Nothing) Then
propertyValue.Text = propInfo.Value.ToString()
End If
propertyValue.Tag = propInfo
If (propInfo.IsLocked) Then
propertyValue.ReadOnly = True
End If
_validator.SetIconPadding(propertyValue, -20)
AddHandler propertyValue.Enter, New EventHandler(AddressOf _
propertyValue_Enter)
AddHandler propertyValue.KeyUp, New KeyEventHandler(AddressOf _
propertyValue_KeyUp)
AddHandler propertyValue.TextChanged, _
New EventHandler(AddressOf _
propertyValue_TextChanged)
AddHandler propertyValue.Validating, _
New CancelEventHandler(AddressOf _
propertyValue_Validating)
panel.Controls.Add(propertyLabel, 0, _currentRow)
panel.Controls.Add(propertyValue, 1, _currentRow)
Dim width As Integer = propertyLabel.Width
propertyLabel.AutoSize = False
propertyLabel.Width = width
propertyLabel.Height = propertyValue.Height
propertyLabel = Nothing
propertyValue = Nothing
_currentRow = _currentRow + 1
Next
_currentRow = 0
End Sub
Validating the input values for a given property can be tricky, since you must first take an input value and convert it into a type supported by the PropertyInformation object's declared ConfigurationValidator—just passing a string instance won't do. Luckily, the PropertyInformation class exposes a Converter property that returns a TypeConverter for the type that the PropertyInformation object can hold. Unfortunately, you can't be certain that every PropertyInformation object you run across in the ConfigurationElement editor will know what TypeConverter to use; some types don't specify one, in which case you'll have to assume that the correct type to store the value in is just a string.
Another problem is one of type identity. You may have a Type object describing what type the PropertyInformation object can hold, but there may be no way of actually doing a cast at run time to the declared Type. Since you're doing this all in a late-bound fashion, you have to box the resulting value inside an Object and pass that to your validator's Validate method.
The validator for a given PropertyInformation object is returned by the aptly named Validator property. ConfigurationValidators expose two public methods that are relevant to this project: CanValidate and Validate. The CanValidate method takes a System.Type as its argument and returns a Boolean indicating whether it knows how to validate that type of object. The Validate method takes an Object as its argument and, unlike CanValidate, doesn't return a Boolean indicating the success or failure of the validation, but rather throws an exception should any problem in validating the supplied object arise. This behavior enables the Validate method to provide much greater detail as to why it can't validate the passed value—the value could be of the wrong type, for instance, or the passed value could be Nothing. A simple Boolean flag wouldn't really help in those types of situations, and it wouldn't allow you to provide descriptive feedback to your users.
Since a validator throws an exception, you have to wrap your call to Validate in a Try/Catch block and catch any exception that's thrown. This is actually quite handy, since you'll just be taking the contents of the Message property on the thrown exception, and using that as the error message to display to the end users with an ErrorProvider.
Now that you've got an ElementEditor control all set up to edit ConfigurationElements, its time to move on to editing collections, which is a bit trickier. Since you don't know what type of object a ConfigurationElementCollection can hold, only that it can hold an object derived from ConfigurationElement, how do you create a new element for a particular implementation of ConfigurationElementCollection?
The answer, as it turns out, is making a call to the CreateNewElement method on ConfiguationElementCollection. CreateNewElement is overridden by the implementers of a given ConfigurationElementCollection, and by convention should return a reference to a new ConfigurationElement object of the appropriate type. CreateNewElement is declared as Protected, meaning it's only available to the class in which it is defined and to any base classes. In order to call this method, you must use reflection to get a reference to the MethodInfo object that describes the method, then call the Invoke method on your MethodInfo reference to invoke the method on your ConfigurationElementCollection reference. At this point, you just pass the returned ConfigurationElement object to an instance of ElementEditor for editing purposes.
That done, you now have a new ConfigurationElement instance all set up and initialized that needs to be added to ConfigurationElementCollection. But how? Again, reflection is the answer.
ConfigurationElementCollection exposes a number of protected members that you'll need to access. In particular, you need the BaseAdd method for adding new items, the BaseIndexOf method for finding the ordinal of an item housed in the collection, and the BaseRemoveAt method for removing an item from the collection at a specific index. Since the control could potentially perform numerous add and remove operations on its target collection, I decided to initialize MethodInfo objects for these methods in the constructor of the control.
Invoking the various protected methods through the MethodInfo objects is straightforward. The MethodInfo class defines an Invoke method that takes two arguments: a reference to the ConfigurationElementCollection object that you want to invoke the method against and an Object array that holds the various arguments this method might accept. For instance, the call to CreateNewElement looks something like this:
Dim configElement As ConfigurationElement = CType( _
createElement.Invoke(colInstance, Nothing), _
ConfigurationElement)
In this case colInstance is the ConfigurationElementCollection instance and createElement is the MethodInfo instance for CreateNewElement. Since the MethodInfo Invoke method returns an Object—again, using reflection, the reflected MethodInfo can't be strongly typed—you must first cast this value to the desired type, ConfigurationElement.
It's a similar story for BaseAdd, BaseIndexOf, and BaseRemoveAt. BaseAdd and BaseRemoveAt return no value, so don't pay any attention to the returned value from the Invoke method. BaseIndexOf, however, does return a value, this time an Integer. A call to that method looks something like this:
Dim elementIndex As Integer = CInt( _
getElementIndex.Invoke(colIntance, new Object(){myElement}))
myElement is the ConfigurationElement you're looking for, and getElementIndex is the MethodInfo instance for BaseIndexOf.
With the index of a specific element, you can then make a call to, for instance, BaseRemoveAt, which would remove the element at the specified index:
removeAtIndex.Invoke(colInstance, new Object(){elementIndex})
It's important to remember that all these hoops you're jumping through to call these methods are due to the fact that the methods themselves are declared Protected, and are thus inaccessible to your code without using reflection. If you had inherited from ConfigurationElementCollection and wanted to call these methods from within your derived class, you'd just call them directly.
As I said before, the collection editor control is a user control; this is primarily to simplify the construction of the control, since it's much easier to achieve the desired layout using the Windows Forms designer (especially when dealing with a composite control like the one I've constructed). The control itself consists of a ListView control and three buttons: Add, Edit, and Remove. The ListView control is populated by the items contained in the collection you're editing, and the ListView columns all correspond to the PropertyInformation objects found on the contained ConfigurationElements. Additionally, the Edit and Remove buttons should only be enabled when an item from the collection displayed in the ListView is selected. The finished control, hosted in the ConfigurationDialog, is shown in Figure 9.
Figure 9 Editing a ConfigurationElementCollection
One challenge in constructing this control was determining the headers to display. Since the ConfigurationElementCollection doesn't contain any info regarding the different PropertyInformation objects returned by the ConfigurationElements it contains, just grab one of the ConfigurationElements in the collection to enumerate the PropertyInformation objects. What if the ConfigurationElementCollection is empty? Make a call to CreateNewElement (through the MethodInfo object created in the control's constructor), enumerate the names (or descriptions) of the PropertyInformation objects of the returned ConfigurationElement, and use those names as the ListView headers (see Figure 10).

Figure 10 Determining List View Headers
Private Sub ProcessElementCollection(ByRef elementCollection As _
ConfigurationElementCollection, ByRef list As ListView)
Dim propInfos() As PropertyInformation
list.Clear()
If (elementCollection.Count > 0) Then
Dim elements(elementCollection.Count - 1) As ConfigurationElement
elementCollection.CopyTo(elements, 0)
ReDim propInfos(elements(0).ElementInformation.Properties.Count _
- 1)
elements(0).ElementInformation.Properties.CopyTo(propInfos, 0)
Else
Dim element As ConfigurationElement
element = CType(_createNewElement.Invoke(_
elementCollection, Nothing), ConfigurationElement)
ReDim propInfos(element.ElementInformation.Properties.Count - 1)
element.ElementInformation.Properties.CopyTo(propInfos, 0)
End If
For Each propInfo As PropertyInformation In propInfos
Dim header As New ColumnHeader()
Dim headerText As String
If (propInfo.Description = String.Empty) Then
headerText = propInfo.Name
Else
headerText = propInfo.Description
End If
header.Text = headerText
list.Columns.Add(header)
Next
For Each subElement As ConfigurationElement In _
CType(elementCollection, ConfigurationElementCollection)
Dim subItem As ListViewItem = Nothing
For Each propInfo As PropertyInformation In propInfos
If (subItem Is Nothing) Then
subItem = New ListViewItem( _
subElement.ElementInformation.Properties(_
propInfo.Name).Value.ToString())
Else
If (propInfo.Value IsNot Nothing) Then
subItem.SubItems.Add(_
subElement.ElementInformation.Properties( _
propInfo.Name).Value.ToString())
Else
subItem.SubItems.Add("")
End If
End If
Next
If (subItem IsNot Nothing) Then
subItem.Tag = subElement
list.Items.Add(subItem)
End If
Next
Dim headerWidth As Integer = list.Width / list.Columns.Count
For Each header As ColumnHeader In list.Columns
header.Width = headerWidth
Next
End Sub
Removing a ConfigurationElement from a ConfigurationElementCollection by using the control is simple. But in order to edit a newly added ConfigurationElement, or edit an existing one, you need another dialog to pop up. To handle this, construct a new dialog that hosts a single instance of the previously created EditControl, which you'll use to pop up a separate window for editing a ConfigurationElement inside a ConfigurationElementCollection. You'll display this dialog from the event handler of the Add and Edit buttons click event.
Final Details
With the two edit controls constructed—one fo