Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use XDocument.Save to save a file using custom indentation for attributes

My goal is to output a modified XML file and preserve a special indentation that was present in the original file. The objective is so that the resulting file still looks like the original, making them easier to compare and merge through source control.

My program will read a XML file and add or change one specific attribute.

Here is the formatting I'm trying to achieve / preserve:

<Base Import="..\commom\style.xml">
  <Item Width="480"
        Height="500"
        VAlign="Center"
        Style="level1header">
(...)

In this case, I simply wish to align all attributes past the first one with the first one.

XmlWriterSettings provides formatting options, but they won't achieve the result I'm looking for.

settings.Indent = true;
settings.NewLineOnAttributes = true;

These settings will put the first attribute on a newline, instead of keeping it on the same line as the node, and will line up attributes with the node.

Here is the Load call, which asks to preserve whitespace:

MyXml = XDocument.Load(filepath, LoadOptions.PreserveWhitespace);

But it seems like it won't do what I expected.

I tried to provide a custom class, which derives from XmlWriter to the XDocument.Save call, but I haven't managed to insert whitespace correctly without running into InvalidOperationException. Plus that solution seems overkill for the small addition I'm looking for.

For reference, this is my save call, not using my custom xml writer (which doesn't work anyway)

XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.NewLineOnAttributes = true;
settings.OmitXmlDeclaration = true;
using (XmlWriter writer = XmlWriter.Create(filepath + "_auto", settings))
{
    MyXml.Save(writer);
}
like image 954
A.Boulianne Avatar asked Jun 23 '17 16:06

A.Boulianne


1 Answers

I ended up not using XDocument.Save altogether, and instead created a class that takes the XDocument, an XmlWriter, as well as a TextWriter. The class parses all nodes in XDocument, TextWriter is bound to the file on disk, which XmlWriter uses as its output pipe.

My class then uses the XmlWriter to output xml. To achieve the extra spacing, I used the solution described here, https://stackoverflow.com/a/24010544/5920497 , which is why I also use the underlying TextWriter.

Here's an example of the solution.

Calling the class to save the document:

XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.NewLineOnAttributes = false; // Behavior changed in PrettyXmlWriter
settings.OmitXmlDeclaration = true;

using(TextWriter rawwriter = File.CreateText(filepath))
using (XmlWriter writer = XmlWriter.Create(rawwriter, settings))
{
    // rawwriter is used both by XmlWriter and PrettyXmlWriter
    PrettyXmlWriter outputter = new PrettyXmlWriter(writer, rawwriter);
    outputter.Write(MyXml);

    writer.Flush();
    writer.Close();
}

Inside PrettyXmlWriter:

private XmlWriter Writer { get; set; }
private TextWriter InnerTextWriter { get; set; }

public void Write(XDocument doc)
{
    XElement root = doc.Root;
    WriteNode(root, 0);
}

private void WriteNode(XNode node, int currentNodeDepth)
{
    if(node.NodeType == XmlNodeType.Element)
    {
        WriteElement((XElement)node, currentNodeDepth);
    }
    else if(node.NodeType == XmlNodeType.Text)
    {
        WriteTextNode((XText)node, currentNodeDepth, doIndentAttributes);
    }
}

private void WriteElement(XElement node, int currentNodeDepth)
{
    Writer.WriteStartElement(node.Name.LocalName);

    // Write attributes with indentation
    XAttribute[] attributes = node.Attributes().ToArray();

    if(attributes.Length > 0)
    {
        // First attribute, unindented.
        Writer.WriteAttributeString(attributes[0].Name.LocalName, attributes[0].Value);

        for(int i=1; i<attributes.Length; ++i)
        {
            // Write indentation
            Writer.Flush();
            string indentation = Writer.Settings.NewLineChars + string.Concat(Enumerable.Repeat(Writer.Settings.IndentChars, currentNodeDepth));
            indentation += string.Concat(Enumerable.Repeat(" ", node.Name.LocalName.Length + 1));
            // Using Underlying TextWriter trick to output whitespace
            InnerTextWriter.Write(indentation);

            Writer.WriteAttributeString(attributes[i].Name.LocalName, attributes[i].Value);
        }
    }

    // output children
    foreach(XNode child in node.Nodes())
    {
        WriteNode(child, currentNodeDepth + 1);
    }

    Writer.WriteEndElement();
}
like image 98
A.Boulianne Avatar answered Nov 11 '22 09:11

A.Boulianne