Click here to Skip to main content
Click here to Skip to main content

TreeConfiguration - configuration made as simple as it gets (or sets)

, 2 Nov 2005
Rate this:
Please Sign up or sign in to vote.
Manage configuration data with a few lines of code. Very few.

TreeConfiguration

Contents

  1. Introduction
  2. Meet the candidate: the TreeConfiguration
  3. Design considerations
  4. Using the code
  5. Can't wait to try it! What's in the package?
  6. Epilogue
  7. 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:

  • There is an almost infinite number of different development scenarios. For each of them there is a particular configuration management implementation that fits in better than any other—someone might find the TreeConfiguration particularly convenient for the task at hand.
  • Even if the TreeConfiguration turns out to be yet another "me too" implementation—which I hope it won't—something can probably be learned from the code. (As it stands, this is true for both good code and some bad code.)

    Among other things, TreeConfiguration code demonstrates how to:

    • Design classes that provide intended functionality;
    • Use indexer and named properties to make the code that consumes those classes simple and readable;
    • Build one possible data storage implementation for the TreeConfiguration class using XML files;
    • Embed an XML schema into assembly resources and use it at run-time to validate integrity of configuration data;
    • Use generic .NET type conversion mechanism to translate an object to string and back;
    • Test all functionality using a minimalistic, do-it-yourself testing framework;
    • Build the project documentation using Visual Studio XML comments stored in external files.

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:

// Set values, create tree structure on the fly if necessary

cfg["/database/login/username"] = "letmein";  // primitive type
cfg["/application/windows/main/rectangle"] = 
  new Rectangle(100, 200, 300, 400); // not-quite-primitive type

// Get values back

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:

TreeConfiguration

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,

// Access the node 'node2' directly
node2 = cfg["/node1/node2"];   // this won't work

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"];
// access subnodes of the node '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"];
// access all keys of the node 'node3'

ICollection values = cfg.Values["/node1/node2/node3"];
// access all values of the node '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:

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:

// Create a new configuration instance,
// specifying physical storage location (an XML file)
XmlConfiguration cfg = 
  new XmlConfiguration(Environment.GetFolderPath(
      Environment.SpecialFolder.LocalApplicationData) + 
  Path.DirectorySeparatorChar + @"MyApplication\MyApplication.cfg",    
  "My Configuration Title");

cfg.Load();
// Load any existing data from the underlying XML file

// Read configuration settings
string username = (string) cfg["/Database/Login/Username"];
int count = (int) cfg["UsageCount"];  // root key syntax
// leading path separator is optional; same as "/Confirmations/WarnOnExit"
bool warnOnExit = (bool) cfg["Confirmations/WarnOnExit"];
Rectangle rect = (Rectangle) cfg["Windows.Main.Rectangle"];
// alternative syntax (configurable)

// Store current settings back at some later moment
cfg["/database/login/username"] = "letmein"; // path is case-insensitive
cfg["/UsageCount"] = 123;                       
cfg["Confirmations/WarnOnExit"] = true;
cfg["/Windows/Main/Rectangle"] = new Rectangle(100, 200, 300, 400);

cfg.Save();
// Save configuration data back to the underlying XML file

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:

// Check if the key exists
bool valueExists = cfg["/Database/Login/Username"] != null;

// Remove the key 
cfg["/Database/Login/Username"] = null;
// nodes 'Database' and 'Login' are not removed

// Search for particular node
ConfigurationNode n = cfg.FindNode("/Database/Login");
bool nodeExists = n != null;

// Check if node contains any subnodes or key/value pairs
bool hasSubNodes = n.HasSubNodes;
bool hasKeys = n.HasKeys;
bool isEmpty = n.Empty;
// true if node doesn't have any subnodes nor key/value pairs

// Remove node
bool removed = cfg.RemoveNode("/Database/Login");
// node 'Database' stays

// Remove everything
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:

// Enumerate all subnodes of the node node3

ICollection subNodes = cfg.Nodes["/node1/node2/node3"]; 
foreach (ConfigurationNode n in subNodes)
{
  // Do something with the subnode
}

// Enumerate all keys of the node node3
ICollection keys = cfg.Keys["/node1/node2/node3"];
foreach (string key in keys) 
{
  // Do something with the key
}

// Enumerate all values of the node node3
ICollection values = cfg.Values["/node1/node2/node3"];
foreach (object value in values) 
{
  // Do something with the value
}

(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(...);
// use whatever culture currently set as CultureInfo.CurrentCulture

cfg.Culture = "fr-FR";    // use French - France culture info

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(...);
// default: CultureInfo.CurrentCulture

float number = 123.456F;
DateTime now = DateTime.Now;
// Set the French culture for the whole configuration
cfg.Culture = "fr-FR";
cfg["/french/number"] = number;  // will be stored as "123,456"
cfg["/french/date"] = now;       // will be stored as "25/10/2005 19:29"

// Override culture info for a child node
ConfigurationNode rootNode = cfg.FindNode("/");
ConfigurationPath path = new ConfigurationPath("southafrican");
// "/southafrican" node

ConfigurationNode subNode = rootNode.CreateSubNode(path);
subNode.Culture = "af-ZA";  
subNode["number"] = number; // will be stored as "123.456"
subNode["date"] = now;      // will be stored as "2005/10/25 07:29 PM"

(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
    • Initial version.
  • 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Vladimir Klisic
Web Developer
France France
Vladimir Klisic is a half-human, half-software development engineer, currently working for Trema Laboratories in southeastern France (Sophia-Antipolis).

Comments and Discussions

 
GeneralSmall bug in &quot;Load()&quot; PinmemberMaugli30-Nov-05 7:57 
GeneralRe: Small bug in &amp;quot;Load()&amp;quot; PinmemberVladimir Klisic1-Dec-05 21:06 
QuestionWhy culture info? PinmemberMaugli26-Nov-05 13:22 
AnswerRe: Why culture info? PinmemberVladimir Klisic27-Nov-05 23:55 
GeneralRe: Why culture info? PinmemberMaugli28-Nov-05 6:07 
GeneralRe: Why culture info? PinmemberVladimir Klisic28-Nov-05 7:27 
GeneralRe: Why culture info? PinmemberMaugli28-Nov-05 11:17 
GeneralRe: Why culture info? PinmemberVladimir Klisic29-Nov-05 21:57 
GeneralXmlConfiguration Collection Pinmemberxoanteis23-Oct-05 23:50 
GeneralRe: XmlConfiguration Collection PinmemberVladimir Klisic6-Nov-05 8:16 
Your suggestion slightly reminds me of a DataTable/DataSet kind of implementation... sounds interesting but, unfortunately, currently I don't have the time to work on it.
 
In the meantime, you are more than welcome to make your own extended version. Good luck!

GeneralComments inside xml files Pinmemberxoanteis21-Oct-05 3:11 
GeneralRe: Comments inside xml files PinmemberVladimir Klisic23-Oct-05 5:39 
QuestionHow to programatically create enumerations? Pinmembertwehr1-Mar-05 9:27 
AnswerRe: How to programatically create enumerations? PinmemberKlisic Vladimir1-Mar-05 22:39 
GeneralBIG warning Pinmemberpprchal28-Feb-05 0:59 
GeneralRe: BIG warning PinmemberVladimir Klisic28-Feb-05 2:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141220.1 | Last Updated 2 Nov 2005
Article Copyright 2005 by Vladimir Klisic
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid