Introduction
This tip is about changing and restoring settings kept in XML
files. To test machines where I work, we need to change
usually three or four settings kept in local XML files. With
a little Dictionary
setup in code and two helper
classes, it's easy to make changes for testing and to restore
settings thereafter.
Thank You, Microsoft
Microsoft made two wonderful classes, XmlReader
and
XmlWriter
, exactly for this purpose. The trick
is knowing which features and which XmlNodeType
's to
be aware of and to handle. In the Example1.xml file shown
above, suppose we want to make the following temporary changes:
- Change
Element1
's text value from "Text1" to "New Text".
- Change
Element2
's text value if it has an attribute with value
"Value1
".
- Change the text value of the first element in
Element3
which has
an attribute value of "NestedElementAttrubeBValue
".
That third possible change is a little dubious. You need to
know which nested elements have text values and what attributes
may be scanned. If Element3
where:
<Element3 AttributeA="NestedElementAttributeBValue">
<AnyOldNestedElement>Replace This Text
</AnyOldNestedElement>
</Element3>
The same text string would be replaced, this time for element
AnyOldNestedElement
since Element3
has no text value of its own.
Notice we are changing element text values, but not any
attribute values. It turns out changing attribute values is
just not needed, at least in my current world. If you need
this, the demo project may still be a good starting point.
Also consider this XML file:
="1.0"="UTF-8"
<root>
<element>One</element>
<element>Two</element>
</root>
Without any attributes to hang its hat on, it's not possibly with
this current code to change just the second element's text value
of Two
. You would need to change the code to replace only
the Nth matching element.
Dictionaries are the Key
Here is a diagram of the Dictionary
s used to save
and restore element text:
Two separate Dictionary
s are used for each of the
sample XML files in the demo project. If you are sure of no
collisions in your XML files, the same two Dictionary
s
could be used for multiple XML files. Here is part of the
setup code in the InitDicts
method for the Dictionary
s
used to save and restore text in Example1.xml:
saveRestoreDict1 = new Dictionary<string, Tuple<string, string, int>>();
replRewriteDict1 = new Dictionary<string, Tuple<string, string, int>>();
saveRestoreDict1["Element1"] =
new Tuple<string, string, int>(null, null, 1);
replRewriteDict1["Element1"] =
new Tuple<string, string, int>(null, "New Text", 1);
XML from the Wild
The two other example XML files in the demo project (Config2.xml
and wpml-config.xml) came from the internet and are little changed
here. They each have or use features I didn't address before
finding them for this demo.
Config2.xml pointed out the need to handle root elements
specifying namespaces. You can't just WriteStartElement(elementName)
if a namespace is involved. You need to supply both the
element name and namespace name. This is a snippet from the
XmlNodeType.Element:
case in the Replace
method of XmlReWriter
:
newNS = reader.LookupNamespace("");
if (newNS != currentNS)
{
writer.WriteStartElement(reader.Name, newNS);
currentNS = newNS;
}
else
{
writer.WriteStartElement(reader.Name);
}
writer.WriteAttributes(reader, true);
wpml-config.xml is the largest example here, but it is actually
only an XML fragment since it has no XmlDeclaration
.
When an XML file is read, if an XmlNodeType.XmlDeclaration
is encountered, you know you have a ConformanceLevel.Document
document. If not, the conformance is only ConformanceLevel.Fragment
.
ConformanceLevel
is part of XmlWriterSettings
.
It needs to be set so the rewritten XML file has or doesn't have
an XML declaration, depending on the original file.
Handling this is actually spread over the app and both helper
classes. In the MainWindow
class, we create a
global boolean and use it as an out
argument in
saving original XML values:
private bool readConformance;
...
saveSuccess = saveReader.Save(saveRestoreDict1, out readConformance);
In XmlSaveReader
we set the boolean if we see an XmlDeclaration
:
public bool Save(Dictionary<string, Tuple<string, string, int>> saveElemVals, out bool xmlDoc)
{
...
bool docConformance = false;
...
case XmlNodeType.XmlDeclaration:
docConformance = true;
break;
...
xmlDoc = docConformance;
return true;
And in XmlReWriter
, this value is passed as the conformDoc
argument in the Replace
method:
public bool Replace(Dictionary<string, Tuple<string, string, int>> replElemVals, bool conformDoc)
{
...
var settings = new XmlWriterSettings() { Indent = true };
if (conformDoc)
settings.ConformanceLevel = ConformanceLevel.Document;
else
settings.ConformanceLevel = ConformanceLevel.Fragment; ?>
Ninja Walking on Rice Paper
The objective is to make rewrites and restores after rewrites as
undisturbing as possible. Ideally, you don't want any
evidence of testing left when you are through. Alas, this
demo project does leave a few whitespace wrinkles, and it doesn't
show code to retain original file timestamps. Here are the
whitespace "improvements" left behind after replacing and
restoring:
- Attributes are separated by a single space on a single
line. Notice the widget attributes in Config2.xml before and
after replace and restore.
- Attributes and their values are separated by "=" only, not " =
".
- No space is left between the last
XmlDeclaration
attribute and
the closing "?>
" marker.
Using the Demo
The downloadable demo was built with Visual Studio 2013, but it
should also be possible to build with 2010 or 2012. Use of Tuple
s
requires .NET 4.0 or better. Once you rebuild the solution
and start the application, you should see a UI similar to the above.
Use the ". . ." button to select one of the samples. Then
click "Save Values". The original XML file is displayed and
original text values for some elements are saved in a Dictionary
.
At this point, "ReWrite
" will replace text values (for 'testing')
and "Restore
" will restore original text values (the Ninja
leaves).
The demo project has two folders, OrigXMLs and TestXMLs.
We play with the files in TestXMLs. To get
clean, untampered files after testing or experimenting, copy files
from OrigXMLs to TestXMLs. You
must reset the Read Only attribute of the copies in TestXMLs
for rewriting and restoring.
References
History
- Submitted to CodeProject on 24th March, 2014