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. |
|