Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Rewriting XML Files

5.00/5 (2 votes)
25 Mar 2014CPOL4 min read 17.9K   144  
Rewriting and restoring XML files (streams) with two helper classes
Image 1

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:

XML
<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:

XML
<?xml version="1.0" encoding="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 Dictionarys used to save and restore element text:

Image 2

Two separate Dictionarys 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 Dictionarys could be used for multiple XML files. Here is part of the setup code in the InitDicts method for the Dictionarys used to save and restore text in Example1.xml:

C#
saveRestoreDict1 = new Dictionary<string, Tuple<string, string, int>>();
replRewriteDict1 = new Dictionary<string, Tuple<string, string, int>>();

//-// Setup dictionaries for Example1.xml

// Tuple Item1 == null means no attribute value needed for match,
//  just the element name (the Key "Element1" in Dictionary saveRestoreDict1)
// Tuple Item2 (null) is set by XmlSaveReader.Save()
// Tuple Item3 (1) is the number of restorations to make.
saveRestoreDict1["Element1"] =
   new Tuple<string, string, int>(null, null, 1);

// Tuple Item1 == null means no attribute value needed for match.
// Tuple Item2 ("New Text") is the replacement text value.
// Tuple Item3 is the number of replacements to make.
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:

C#
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:

C#
private bool readConformance;
   ...
   saveSuccess = saveReader.Save(saveRestoreDict1, out readConformance);

In XmlSaveReader we set the boolean if we see an XmlDeclaration:

C#
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:

C#
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; // No <?xml ... ?>

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)