
Contents
- Introduction
- Meet the candidate: the TreeConfiguration
- Design considerations
- Using the code
- Can't wait to try it! What's in the package?
- Epilogue
- History
Introduction
I can almost hear you say "Oh no, not another one". Thanks to the TreeConfiguration
, configuration management implementations have finally joined Outlook Bar controls, RSS readers and few others on the 'Top Ten Most Reinvented Code of All Times' chart.
Hey, wait! Don't give up reading yet!
I really didn't want to reinvent the wheel, and I hope I didn't. It's just that I already had the code long before I even started thinking about writing an article. I did know about at least five other implementations presented here on CodeProject, yet I still decided to go about publishing my own. Here are the two main excuses motives for that:
Hopefully that should be enough reasons to justify not only the time spent writing an article on a worn out topic, but also the time spent reading it.
Meet the candidate: The TreeConfiguration
The implementation you are about to see places all bets on the simplicity of use and the ability to maintain data in a flexible structure. Because different people interpret terms 'simplicity' and 'flexibility' differently, it's better to let the code speak for itself.
Here is the essence of it:
cfg["/database/login/username"] = "letmein";
cfg["/application/windows/main/rectangle"] =
new Rectangle(100, 200, 300, 400);
string username = (string) cfg["/database/login/username"];
Rectangle rect = (Rectangle)
cfg["application/windows/main/rectangle"];
It can't get much simpler than that, but you are probably old enough to know that there is no such thing as a free lunch�the simplicity comes with a small price tag on it.
The next section discusses some of the issues that have been taken into consideration in the design phase, hopefully explaining in more detail the reasoning that led to one such implementation. If you only want to take a look at the code, you can skip the next section and go directly to the section named Using the code.
Design considerations
Hierarchical structure
Maintaining configuration data in a tree-like hierarchy has the advantage over the traditional Windows .INI file approach because it better models the actual structure of the data. TreeConfiguration
retains the .INI file concept of key/value pairs, because that's what individual settings are. But instead of being maintained in some kind of a list, key/value pairs become leaves in a hierarchy of nodes where each node has a name, zero or more key/value pairs, and zero or more subnodes.
The following picture visualizes that structure:

By itself, the above hierarchical structure doesn't bring any particular improvement over any other meaningful way of organizing data. It's just that, when complemented with simple and consistent syntax, it makes the code more readable and easier to use.
Simple and consistent syntax
Each individual configuration setting (the term 'key' is equally used in this article) in the hierarchy is referenced using an absolute path to it. Path is a case-insensitive string that has the following form:
/<node>/<node>/.../<node>/<key>
The TreeConfiguration
class implements an indexer that sets and gets a concrete setting identified by its path:
string value = cfg["/node1/node2/node3/key"];
cfg["/node1/node2/node3/key"] = value;
The configuration[<path>] syntax is a shorthand for reading or writing the value of a single setting without having to access its node directly (in the previous example, the node "node3"). Accessing single settings this way is convenient; however, there will be times when individual nodes will have to be accessed directly�for example, to examine a node's subnodes.
Unfortunately, subnodes cannot be accessed using the same syntax,
node2 = cfg["/node1/node2"];
because the indexer will look for the key "node2" under the node "node1", not for the node "/node1/node2". Obviously, the path cannot refer to both nodes and keys at the same time, so it always gets interpreted as a path to a key.
Yet, the goal is to have a consistent syntax for accessing any part of the configuration.
This is where parameterized named properties (a concept borrowed from Visual Basic .NET) come to the rescue. This feature isn't supported directly in the current C# version, but it can be simulated effectively. The TreeConfiguration
class exposes a member called Nodes
that itself implements an indexer. Unlike TreeConfiguration
's indexer, Nodes
' indexer interprets the path passed to it as a reference to a node. Being a nested type, Nodes
has the access to TreeConfiguration
's internal node hierarchy so its indexer can find the node identified by the path and return a collection of its subnodes:
ICollection subNodes =
cfg.Nodes["/node1/node2/node3"];
The above technique gives a rather convenient access to subnodes of any node, but why stop there? A node's keys and values can be accessed in exactly the same way:
ICollection keys = cfg.Keys["/node1/node2/node3"];
ICollection values = cfg.Values["/node1/node2/node3"];
The whole idea of using the path to select concrete nodes slightly resembles XPath expressions. However, in order to keep things simple, the path syntax is intentionally kept in its simplest form (no wildcards etc.). When used in this context, the path serves as a node selector and three different TreeConfiguration
members conveniently expose three different parts of a node's content through the ICollection
interface.
Concrete collection implementation (currently HybridDictionary
) is intentionally hidden from the public interface in order to allow eventual replacement by another collection that performs better without breaking code that uses TreeConfiguration
.
XML-based data storage
TreeConfiguration
class itself does not impose any requirements regarding the underlying data storage. In fact, it doesn't even offer any mechanism for storing the data. Derived classes must implement virtual methods Load
and Save
and provide their own means of data retrieval and storing.
The source code package for this article comes with one concrete TreeConfiguration
implementation, the XmlConfiguration
class that uses the XML file for its underlying physical storage. XML was a natural choice because it conveniently matches the given hierarchical structure.
Here is how nodes, keys and values get translated in the XML file maintained by XmlConfiguration
:

Even though it wasn't strictly necessary, XmlConfiguration
class internally uses the XML schema to validate the content of the underlying XML configuration file before doing anything with it. To avoid any problems locating it when it's needed, the schema is stored as an embedded resource in the same assembly that contains the TreeConfiguration
implementation (as opposed to being loaded as an external file).
Type conversion
Individual configuration settings are concrete instances of various types. To persist them in an XML file, all those different objects must be serialized to strings in such a way that they can be identically reconstructed later. In addition, this problem must be handled in a generic way as object types are not known in advance.
XmlConfiguration
uses a generic type conversion mechanism borrowed from the Visual Studio Property Browser. The Property Browser offers design-time editing of type properties in their textual form. Without knowing in advance what the edited type will be, the Property Browser somehow finds the way to convert its properties to strings, lets the user edit them, and converts them back to their native type. (Not all properties can be edited as strings, but more on that soon.)
Behind the curtains, properties pass through TypeConverter
. TypeConverter
is the central point in the .NET framework type conversion mechanism. The FCL provides several TypeConverter
-derived classes that handle conversion of most common .NET types. Deciding what type converter to use for a particular type is a task for another FCL class: TypeDescriptor
. TypeDescriptor
checks the converter by looking for a TypeConverterAttribute
.
By delegating actual conversion work to TypeConverter
and TypeDescriptor
, XmlConfiguration
benefits from the framework library code that already provides conversion support for a fair number of common FCL types. The mechanism has its limits, though. For example, collections cannot be serialized simply by letting TypeDescriptor
choose the appropriate converter. It could have been possible with some additional coding, but the task can anyway be accomplished with few lines of code walking through the collection and serializing individual elements.
What about custom types?
As you would expect, TypeConverter
concept is extendable. In order to maintain your own type in the XmlConfiguration
, you need to write a type converter for it. A proper type converter is what qualifies the type for usage with XmlConfiguration
. Converters are easy to build: check the CustomTypeConverter.cs file for MyClass
, an example of a custom type, and MyClassConverter
, the corresponding type converter. (MyClass
is used in the accompanying unit tests; check the method TestCustomTypeConverter
in the file Test.cs for details.)
Using the code
Common operations
TreeConfiguration
represents the base class that provides only in-memory data management. Derived classes, such as the XmlConfiguration
class (also part of the package), implement virtual methods Load
and Save
providing concrete data storage implementation.
This is how the typical usage scenario looks like:
XmlConfiguration cfg =
new XmlConfiguration(Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData) +
Path.DirectorySeparatorChar + @"MyApplication\MyApplication.cfg",
"My Configuration Title");
cfg.Load();
string username = (string) cfg["/Database/Login/Username"];
int count = (int) cfg["UsageCount"];
bool warnOnExit = (bool) cfg["Confirmations/WarnOnExit"];
Rectangle rect = (Rectangle) cfg["Windows.Main.Rectangle"];
cfg["/database/login/username"] = "letmein";
cfg["/UsageCount"] = 123;
cfg["Confirmations/WarnOnExit"] = true;
cfg["/Windows/Main/Rectangle"] = new Rectangle(100, 200, 300, 400);
cfg.Save();
A few notes:
- There are only three kinds of elements in the hierarchy: nodes, keys and values.
- The very last segment of the path is considered to be a key that identifies the concrete setting.
- Any segments of the path preceding the key are considered to be tree nodes.
- Value is a concrete instance of a certain type.
- Nodes can contain both child nodes and key/value pairs.
- Nodes and keys get created on the fly if they don't exist.
The only requirement that XmlConfiguration
imposes on values is that they must be convertible to string and back. This requirement actually comes from the fact that the underlying storage is an XML file; the TreeConfiguration
itself does not impose any such requirements.
The list of types that can be managed using the XmlConfiguration
includes all .NET primitive types (string
, int
, DateTime
etc.) and FCL types that support generic type conversion by means of the TypeConverter
class. Types like Point
, Size
and Rectangle
(all from the System.Drawing
namespace) are just some of the examples of FCL types that can be managed using the XmlConfiguration
right out of the box. (You can find a detailed discussion about type conversion issues here.)
The general rule is: whatever type can be edited as a single property in the Visual Studio Property Browser should also be suitable for use with the XmlConfiguration
. For any other types, there is no magic�you have to help to make conversion happen by writing a type converter.
The generic type conversion implies that the managed types are simple enough so that their instances can be represented by a single string. This isn't really a big problem because the individual configuration settings are typically either primitive types or otherwise very simple types such as Rectangle
, Size
etc. (And even if a type instance cannot be represented with a single string, it can always be broken down to pieces that can.)
In any case, the XmlConfiguration
doesn't limit support to only built-in FCL types. It can work with user-defined types as well, provided that you implement a TypeConverter
-derived type converter for them. The source code package comes with a simple example that shows how to build one.
Search and Removal
From time to time you'll want to check if a single key or node exists, remove it, or even erase the complete hierarchy. The following example shows these operations:
bool valueExists = cfg["/Database/Login/Username"] != null;
cfg["/Database/Login/Username"] = null;
ConfigurationNode n = cfg.FindNode("/Database/Login");
bool nodeExists = n != null;
bool hasSubNodes = n.HasSubNodes;
bool hasKeys = n.HasKeys;
bool isEmpty = n.Empty;
bool removed = cfg.RemoveNode("/Database/Login");
cfg.Clear();
Enumeration
Most of the configuration settings are inherently static: they get stored in a fixed, predefined location in the configuration hierarchy and their data size is constant. Managing this kind of settings is easy because everything is known in advance: a setting can be referenced using a hard-coded path and the code can make safe assumptions about the content structure.
On the other side, some configuration settings, such as the list of most recently opened documents, have a dynamic structure: the size of the data is variable and it may even happen that there is no data at all. In terms of configuration elements, a node that represents dynamic settings contains a variable number of subnodes and key/value pairs.
In order to read the content of a dynamic setting, the content has to be made discoverable. For that reason the TreeConfiguration
class exposes the node content in the form of collections of subnodes, keys and values. These three collections are accessible through corresponding named properties Nodes
, Keys
and Values
:
ICollection subNodes = cfg.Nodes["/node1/node2/node3"];
foreach (ConfigurationNode n in subNodes)
{
}
ICollection keys = cfg.Keys["/node1/node2/node3"];
foreach (string key in keys)
{
}
ICollection values = cfg.Values["/node1/node2/node3"];
foreach (object value in values)
{
}
(Make sure you check the file Test.cs for concrete examples.)
Managing culture-specific data
Type conversion quite often results in formatted strings: for example, numbers are presented with decimal and thousands separators, dates are presented with date and time separators etc. The resulting string format is culture-dependent.
Storing culture-sensitive formatted strings without culture information used to produce them is often an expensive mistake. The initial version of the XmlConfiguration
did not store any culture information, often making it impossible to correctly read a configuration file in an environment that has a different regional setting from the one the file was created in.
To reliably convert formatted strings back to concrete types, TypeConverter
needs the information about the culture used for the conversion. This is why the XmlConfiguration
type now exposes a string property named Culture
to identify the culture used in type conversions.
By default, if Culture
property is not set, the XmlConfiguration
will use CultureInfo.CurrentCulture
for all type conversions. When specifying the culture use standard names, as in the following example:
XmlConfiguration cfg = new XmlConfiguration(...);
cfg.Culture = "fr-FR";
Culture info can also be specified for each individual ConfigurationNode
node. Culture info specified for a concrete node applies to all its subnodes and key/value pairs. In fact, setting the XmlConfiguration.Culture
property just sets the Culture
property of the root ConfigurationNode
. If there are no subnodes that override the root node setting, culture info applies to the whole configuration. The following example shows how to set and override culture info:
XmlConfiguration cfg = new XmlConfiguration(...);
float number = 123.456F;
DateTime now = DateTime.Now;
cfg.Culture = "fr-FR";
cfg["/french/number"] = number;
cfg["/french/date"] = now;
ConfigurationNode rootNode = cfg.FindNode("/");
ConfigurationPath path = new ConfigurationPath("southafrican");
ConfigurationNode subNode = rootNode.CreateSubNode(path);
subNode.Culture = "af-ZA";
subNode["number"] = number;
subNode["date"] = now;
(For a more detailed example, check the Test.cs file.)
Can't wait to try it! What's in the package?
The package contains two projects:
- The
TreeConfiguration
class library.
- Files that implement the actual functionality: TreeConfiguration.cs, ConfigurationNode.cs, ConfigurationPath.cs, XmlConfiguration.cs, CustomTypeConverter.cs and TreeConfiguration.xsd;
- Home-made unit testing framework: UnitTest.cs;
- Functionality tests executed by the unit testing framework: Test.cs;
- Documentation: docs\*.xml files, a help file generated by NDoc, and an NDoc project file used to build it.
- A sample console application that does nothing but runs all the tests against the
TreeConfiguration
assembly.
A home-made unit testing framework was added to the package only to avoid having dependencies to a third-party framework that some readers might not have on their machines. Under normal circumstances, something like TestDriven.NET would be more appropriate.
The tests in the Test.cs file not only verify the correctness of the given functionality but also demonstrate all aspects of usage. This file is the first place to look for the answers to any questions you might have.
In order to test the serialization of certain FCL classes, the TreeConfiguration project contains a few otherwise unnecessary references to assemblies such as System.Drawing
and System.Windows.Forms
.
To build the documentation help file such as the one that is part of the package (TreeConfiguration.chm), you will need NDoc. The project source code files themselves contain no comments�only references to external files where the actual comments are. When the project gets built, all those references to individual documentation files in the docs subfolder get analyzed and the corresponding files get compiled into a single resulting XML documentation file. NDoc uses that file to build a help file in a number of different formats.
Epilogue
And that pretty much rounds up this idyllic story about the TreeConfiguration
. I just hope that the reality won't distort it too much, and that you'll find the code usable.
History
- 14/02/2005
- 25/10/2005
- Added support for managing data stored in a culture-specific format.
- Fixed reading of manually created XML configuration files that contain comments.