Nimajin home
Home Resume Portfolio Technologies Download Contact

XMLKey

Classes for persisting application state in XML files, modeled on ATL::CRegKey

I was happy persisting my program state to the registry. I've found CRegKey to be very useful for saving application settings such as window position and MRU lists.  One thing that was especially nice about it is that I could access my settings randomly. But now I want to save larger amounts of data, so I think a file will be better. I'm thinking an XML file will be nice, because I'll be able to read it to verify that my stuff is saved correctly, and I can modify it manually if I want. I don't want to write a parser though, so I'll take advantage of MSXML.

I investigated some alternatives, but none did what I wanted:

  • MS IsolatedStorage is only .NET, not COM
    (If anyone knows how to get at IsolatedStorage in unmanaged C++ I'd love to use it because I like how it creates a hidden place to store stuff)
  • ATL has persistence mix-ins for COM objects, but they're not easily adapted for application settings. There's no implementation from the app side.
  • John Torjo's Straightforward Settings Library (from Dr Dobbs) is nice, but it's global, not hierarchical and not XML.
  • VB Settings are managed again.
  • ATLPersistXML persists into properties from streams or files, but uses SAX and is sequential.
  • XMLLite is sequential too.

I guess I want the XML exercise, so I'm making CXMLKey to look like ATL's CRegKey.

Hopefully you're looking for XML persistence and these classes will save you a couple of days work!

Implementation

XMLKey is modelled on ATL's CRegKey. XMLKey only does the basic store and query stuff, not enumerating, multistring, binary, security or Flush. Only the newer CRegKey signatures are implemented (ATLVER >= 0x0700); QueryValue and SetValue are not.

XMLKey consists of three classes: CXMLNode, CXMLDoc and CXMLKey. CXMLNode is a base class for the other two classes. It contains a parent property for linking CXMLDoc and CXMLKey objects in a tree. CXMLKey is the CRegKey replacement. It represents a node in the XML element tree that can contain child nodes or values. CXMLDoc doesn't really have an analog in the registry paradigm - it represents the file that the XML lives in. It replaces the root keys used by CRegKey, like HKCU and HKLM.

A CXMLKey object cannot save or read values until it's linked into the document tree. CXMLKeys are linked into a tree by the Create or Open methods. The Create method connects itself up to the XMLKey specified in the parent property and instantiates a new node in the XML document. It is to be used when creating an XML file. Open is to be used when reading from existing files. It links the key to a parent and to a node in the document, but does not create a node. It fails if there is no matching node in the file. The nodeName parameter of these methods is the tag text used in the XML file. Just like the linkage of CRegKey objects reflects the tree structure of the registry keys they represent, the linkage of the XMLKey objects is the same as the tree structure of the XML file. I'd have preferred that the CRegKey object methods created or opened children instead of linking to parents, but that's life.

<Settings>
  <Main>
    <Left>300</Left>
    <Top>18446744073709551615</Top>
  </Main>
  <Configurations>
    <Count>3</Count>
    <Configuration>
      <Name>Easy</Name>
    </Configuration>
    <Configuration>
      <Name>Normal</Name>
    </Configuration>
    <Configuration>
      <Name>Difficult</Name>
    </Configuration>
  </Configurations>
</Settings>
    

Here's a sample of the XML generated by the demo. The XML is very, very basic, but very flexible. It has no schema and no pre-defined structure. There are XML elements for every CXMLKey. Any CXMLKey can save and read values, whether it was created or opened. Values are stored in the file as child nodes of the CXMLKey's node. All values are saved as text. DWORD & QWORD typed methods are provided, but CXMLKey stores the values as text. Values are stored as XML elements, not as attributes. This is only because I find it easier to read files that are more vertically than horizontally aligned. This generates a bigger file though, because all values have start and end tags, whereas attributes have only start tags. I might still be convinced to use attributes for values. Maybe if I adjust the formatting to newline between attributes...

XML has more capabilities than the registry, especially the ability to add multiple items with same name. Where with CRegKey you'd have to add numbers to item names, e.g. Item1, Item2, Item3, to make them unique, in XML you can add 3 <Item> nodes, each with a different value. This might be a feature, but it can be seen as a complication:

  • When your reading values, how do you differentiate between three with the same name? All of CXMLKey's query methods and Open accept an XPath selection string as the valueName parameter. XPath supports indexing using [i] syntax. Indexing is 1-based. Please note that if you index past the actual number of items, the first item is returned.
  • When you're writing, how do you signal that you want to add another value of the same name to a list, as opposed to overwriting an existing value? CXMLKey's Create and SetStringValue methods add a boolean append parameter to the CRegKey parameters. When this parameter is set, CRegKey will append a duplicate node if one with the same name already exists, otherwise it'll use the existing node.
  • When there are many values with the same name, how do you know how many there are, so you can access them all? I didn't add a method to do this, what I do is have the application save the list length in a Count value. You can see this in the example.

The CXMLDoc constructor accepts a parameter to enable pretty formatting. When this option is enabled, Create and SetStringValue insert line feeds and indents into the XML so it becomes formatted like the example above. Otherwise the XML is one single line of text.

CXMLDoc processes XML using DOM (Domain Object Model), not SAX (Simple API for XML). DOM keeps all of the data in memory while you have it open. With DOM you can randomly access the data by name; you're not subject to the SAX restriction of saving and getting everything in order. DOM uses more memory though.

CXMLDoc does not check for MSXML installed. If it's not installed, the constructor will abort with an HRESULT from xmlDoc.CoCreateInstance in ConstructorResult. CXMLDoc requests MSXML version 4. It uses the simplest functionality though, so you could modify it to request a lesser version.

CXMLDoc::Save overwrites any existing file.

Usage

CXMLKeys can be used the same way the CRegKeys are used. Normally you will open and read all settings on application startup and then open and update or create and store settings on shutdown.

To load an existing file you need to instantiate a CXMLDoc and call its Load method. The CXMLDoc constructor creates and initializes an empty MSXML document instance. If anything fails in the constructor CXMLDoc saves the error code in its ConstructorResult public member.

CXMLDoc XMLSettings;
if (FAILED(XMLSettings.ConstructorResult))
    _tprintf (_T("MSXML instantiation error = %04X\n"), XMLSettings.ConstructorResult);
else 
{
    HRESULT Result = XMLSettings.Load (path);
    if (Result != S_OK)
    {
        USES_CONVERSION;
        CComBSTR Error;
        if (SUCCEEDED(XMLSettings.GetErrorDescription ((BSTR &) Error)))
            _tprintf (_T("SettingsLoad error = %s\n"), OLE2CT(Error));

        return Result;
    }
}

The Load method reads in and parses an existing XML file. The demo application contains a nice SettingsFilePath method to get a file location in the user's Application Data folder. Don't test Load's return value with the FAILED macro. If the file doesn't exist the value is S_FALSE, which is not reported as a failure by FAILED. You can find out what failed in Load by calling GetErrorDescription.

After Load succeeds, the file's entire contents are in RAM and can be accessed using CXMLKey objects.

CXMLKey Settings;
Result = Settings.Open (XMLSettings, _T("Settings"));
if (Result != S_OK)
    return Result;

CXMLKey Main;
Result = Main.Open (Settings, _T("Main"));
if (Result != S_OK)
    return Result;

// Then get the values
DWORD Left;
Result = Main.QueryDWORDValue (_T("Left"), Left);

XML data is structured hierarchically, just like registry data. A CXMLKey represents one node in the XML data tree. An XML data node may contain values and branches. A CXMLKey is a branch and contains the values. Like CRegKey, every CXMLKey must be linked to a parent node. At the root of the tree, a CXMLKey must link to a CXMLDoc. In the code above, the CXMLKey named Settings uses the Open method to link itself to the CXMLDoc named XMLSettings and the Main key links itself as a child of Settings. Open's nodeName parameter is the name of the XML branch that this key will represent. Main can access all of the data contained in Settings/Main. When applied to the XML above, the QueryDWORDValue for "Left" returns the value 300. Like Load, the Open and Query methods return S_FALSE if the requested node or value can't be found in the XML.

A CXMLKey provides access to all of the data of all of its child nodes. This is because the nodeName parameter is actually an XPath selection string. With XPath, you can specify subnodes using '/' delimiters. Thus you can skip opening Main and shortcut to the 'Left' value directly from Settings like this:

CXMLKey Settings;
Result = Settings.Open (XMLSettings, _T("Settings"));
if (FAILED(Result))
    return Result;

DWORD Left;
Result = Settings.QueryDWORDValue (_T("Main/Left"), Left);

XPath provides indexing syntax to access nodes or values from lists where the items all have the same XML tag:

CXMLKey Settings;
Result = Settings.Open (XMLSettings, _T("Settings"));
if (FAILED(Result))
    return Result;

TCHAR Name [24];
DWORD Length = 23;
Result = Settings.QueryStringValue (_T("Configurations/Configuration[3]"), Name, Length);
    

Use the Create method to add a new key to the data hierarchy. This example adds a 'Configurations' node to the root 'Settings' node:

CXMLKey Settings;
Result = Settings.Create (XMLSettings, _T("Settings"));
if (FAILED(Result))
    return Result;

CXMLKey Configurations;
Result = Configurations.Create (Settings, _T("Configurations"));

Create and SetValue do not use XPath parsing on the nodeName parameters like Open and QueryValue. When creating something new you are creating a parent/child relationship and so you must instantiate a CXMLKey for every new level in the hierarchy. You don't have to use Create all the way up the tree though. If you want to add to an existing hierarchy you can use Open to get the parent, and then use Create to add to it:

CXMLKey Configurations;
Result = Configurations.Open (XMLSettings, _T("Settings/Configurations"));
if (FAILED(Result))
    return Result;

Result = Configurations.SetDWORDValue (_T("Count"), 1);

CXMLKey Configuration;
Result = Configuration.Create (Configurations, _T("Configuration"), true);

If XMLSettings already had a 'Settings/Configurations/Configuration' node, because it was loaded from a file or you had previously added that node, Configuration would refer to the existing node as if it were merely Opened. You can force creation of a new node, even one with the same name as one the parent already contains, by setting Create's append parameter true. Doing so puts another node with the same name right beside the existing one. You can add as many duplicates as you wish. You can access the duplicates using indexing syntax as described above.

After you've stored all of your settings in XMLKeys, use XMLDoc's Save method to save the XML file. If the XMLDoc was loaded from a file you can omit the path parameter and Save will update the file you loaded from.

XMLKeys are entirely random access. You can add nodes to settings loaded from a file and resave the file with the new nodes in it. You can delete nodes, but be careful not to delete any parents before you're done with their children. I didn't try to see what happens if you delete XMLDoc before finishing with key nodes.

Observations

The registry is only somewhat like XML files. XML has so many capabilities that are different from the registry or are additions to it, that CRegKey is not the best thing to model an XML persistence system on. On the other hand, XML can duplicate all of the functionality of the registry, but I didn't completely do that here.

Some people like to get their settings from storage whenever the program needs it, as opposed to loading them all up at startup. I'm sure that if you instantiate and load XMLDoc and use XMLKey to get a value very time you need one, it'll be nowhere near as fast as the registry, which is kept in RAM. Maybe performance would be good if you load XMLDoc at startup and keep it loaded all the time the application is running, and just open XMLKeys when you want a value.

You can't structure a file saved by CXMLDoc the same as a .NET Application settings file because CXMLKey stores values in elements rather than attributes.

CXMLKey does not yet provide binary blob support, so it won't be any good for WTL8/atlwince.h/CAppInfoBase. It could drop in to WTL::CRecentDocumentList, though.

I just found out about Pascal Hurni's fabulous Settings Storage at CodeProject! I hope somebody still finds this work useful.

This is my first code article on my web site. I hope eventually that my writing will be focused, complete and concise.

Download Files

Source Code (9 Kb) Includes XMLKey classes, test/demo console application and VS 2005 project file

Requires ATL version 7, available with Visual Studio, or in the Platform SDK

Revisions

2007-12-06 Change Open & Query methods to return S_FALSE when valueName not found and E_ABORT when QueryStringValue(TSTR) has insufficient buffer space.
2007-11-28 Initial release.

Page copyright © 2008 Nimajin Software Consulting, last revised 2009-09-25