|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Contents
IntroductionI can almost hear you say "Oh no, not another one". Thanks to the 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
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 TreeConfigurationThe 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 considerationsHierarchical structureMaintaining 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. 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 syntaxEach 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 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 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 Concrete collection implementation (currently XML-based data storage
The source code package for this article comes with one concrete Here is how nodes, keys and values get translated in the XML file maintained by
Even though it wasn't strictly necessary, Type conversionIndividual 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.
Behind the curtains, properties pass through By delegating actual conversion work to What about custom types? As you would expect, Using the codeCommon operations
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:
The only requirement that The list of types that can be managed using the 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 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 In any case, the Search and RemovalFrom 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();
EnumerationMost 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 // 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 dataType 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 To reliably convert formatted strings back to concrete types, By default, if 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 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:
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 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. EpilogueAnd that pretty much rounds up this idyllic story about the History
|
||||||||||||||||||||||