11

I am working with a legacy application that does not import abbreviated empty xml elements. For example:

BAD empty:

<foo />

GOOD empty:

<foo></foo>

I know the solution to achieve this, which I will present now:

public class XmlTextWriterFull : XmlTextWriter
{


    public XmlTextWriterFull(Stream stream, Encoding enc) : base(stream, enc)
    {
    }

    public XmlTextWriterFull(String str, Encoding enc) : base(str, enc) 
    {
    }

    public override void WriteEndElement()
    {
        base.WriteFullEndElement();
    }
}

and the client code:

                    var x_settings = new XmlWriterSettings();
                    x_settings.NewLineChars = Environment.NewLine;
                    x_settings.NewLineOnAttributes = true;
                    x_settings.NewLineHandling = NewLineHandling.Replace;
                    x_settings.CloseOutput = true;
                    x_settings.Indent = true;
                    x_settings.NewLineOnAttributes = true;

                    //var memOut = new MemoryStream();
                    var writer = new XmlTextWriterFull(outputFilename, Encoding.UTF8); //Or the encoding of your choice
                    var x_serial = new XmlSerializer(typeof(YOUR_OBJECT_TYPE));
                    x_serial.Serialize(writer, YOUR_OBJECT_INSTANCE);

                    writer.Close();

However, if you observed carefully the XmlWriterSettings are never used in the client code. Therefore the xml output is terribly formatted. My questions is this: how do I adapt the above code to accept XmlWriterSettings?

The use of factory creation methods and sealed/internal/abstract classes makes this difficult to implement an override.

I will accept an alternative solution, I am not married to my above solution.

  • WORKAROUND SOLUTION

Step 1: create the following class in your solution:

public class XmlTextWriterFull : XmlTextWriter
{
    public XmlTextWriterFull(TextWriter sink) : base(sink)
    {
        Formatting = Formatting.Indented;
    }

    public override void WriteEndElement()
    {
        base.WriteFullEndElement();
    }
}

Step 2: Add the following client code. Make sure to replace YOUR_OBJECT_TYPE and YOUR_OBJECT_INSTANCE with the class and instance your are working with:

TextWriter streamWriter = new StreamWriter(outputFilename);
var writer = new XmlTextWriterFull(streamWriter);

var x_serial = new XmlSerializer(typeof (YOUR_OBJECT_TYPE));
x_serial.Serialize(writer, YOUR_OBJECT_INSTANCE);

writer.Close();

The workaround above will produce the following empty xml element formatting:

<foo>
</foo>

The issue with this workaround is that it adds a line feed (notice the elements are on separate lines). This may be acceptable for you but causes issues with my legacy application.

sapbucket
  • 6,795
  • 15
  • 57
  • 94
  • Don't know whether it addresses this particular case, but you could use the XmlWriter.Create overload that takes an existing writer and the settings. You could pass your own writer implementation to that method. According to http://msdn.microsoft.com/en-us/library/a09119h4.aspx it allows you to "add additional features to an underlying XmlWriter object", but I don't know whether that includes the formatting. – fsimonazzi Nov 15 '12 at 01:06
  • I have not been able to answer my own question. But I did find an acceptable workaround. I'll post it here to hopefully help someone else; however, I still want a solution to my question. My workaround does not use XmlWriterSettings which is what I want. – sapbucket Nov 15 '12 at 22:54
  • Update: my legacy application does not accept the above workaround because the full tag has a line feed element inserted; therefore the tags appear on separate lines. I am posting a bounty to see if I can draw more attention to the question. – sapbucket Nov 19 '12 at 16:34
  • This is kinda lame and hacky, but you could output postprocess your output and replace `>\s+` with `>` – Ilia G Nov 19 '12 at 17:08
  • Have you had a chance to take a look at my answer? I'm fairly confident it will solve your problem. – nick_w Nov 26 '12 at 19:46
  • Related: [How can I stop empty XML elements self-closing using XmlDocument in C#?](https://stackoverflow.com/q/42959958/3744182) where [this answer](https://stackoverflow.com/a/42960980/3744182) includes a `XmlWriterDecorator`. – dbc Dec 14 '18 at 09:11

5 Answers5

5

How about this.

Grab the awesome XmlWrappingWriter class from http://www.tkachenko.com/blog/archives/000585.html (I have omitted the code for the sake of brevity).

With that, we can create a sub-class as follows (very similar to your original one):

public class XmlTextWriterFull2 : XmlWrappingWriter
{
    public XmlTextWriterFull2(XmlWriter baseWriter)
        : base(baseWriter)
    {
    }

    public override void WriteEndElement()
    {
        base.WriteFullEndElement();
    }
}

It can then be invoked like this (again very similar):

var x_settings = new XmlWriterSettings();
x_settings.NewLineChars = Environment.NewLine;
x_settings.NewLineOnAttributes = true;
x_settings.NewLineHandling = NewLineHandling.None;
x_settings.CloseOutput = true;
x_settings.Indent = true;
x_settings.NewLineOnAttributes = true;

using (XmlWriter writer = XmlWriter.Create(outputFilename, x_settings))
{
    using (XmlTextWriterFull2 xmlTextWriterFull = new XmlTextWriterFull2(writer))
    {
        var x_serial = new XmlSerializer(typeof(YOUR_OBJECT_TYPE));
        x_serial.Serialize(xmlTextWriterFull, YOUR_OBJECT_INSTANCE);
    }
}

In my case, an element that had previously been rendered as

<Foo>
</Foo>

became

<Foo></Foo>

As you alluded to in your question, this is actually quite a tricky problem due to everything being sealed/internal etc., making overrides rather difficult. I think my biggest problem was trying to get an XmlWriter to accept XmlWriterSettings: beyond this approach, I could find no way of getting the original XmlTextWriterFull to respect the given XmlWriterSettings.

MSDN states that this method:

XmlWriter.Create(XmlWriter, XmlWriterSettings)

Can be used to apply the XmlWriterSettings to the XmlWriter. I couldn't get this to work like I wanted (the indentation never worked, for example), and upon decompiling the code, it does not appear that all the settings are used with this particular method, hence why my invocation code just passes in the outputFile (a stream of some sort would work just as well).

nick_w
  • 14,758
  • 3
  • 51
  • 71
2

The workaround solution you gave in your question adds extra line breaks (when indenting is enabled) because we're telling the writer to treat this element as if it had children.

Here is how I modified your workaround to manipulate the indenting dynamically so as to avoid those extra line breaks.

public class XmlTextWriterFull : XmlTextWriter
{
    public XmlTextWriterFull(TextWriter sink)
        : base(sink)
    {
        Formatting = Formatting.Indented;
    }

    private bool inElement = false;

    public override void WriteStartElement(string prefix, string localName, string ns)
    {
        base.WriteStartElement(prefix, localName, ns);

        // Remember that we're in the process of defining an element.
        // As soon as a child element is closed, this flag won't be true anymore and we'll know to avoid messing with the indenting.
        this.inElement = true;
    }

    public override void WriteEndElement()
    {
        if (!this.inElement)
        {
            // The element being closed has child elements, so we should just let the writer use it's default behavior.
            base.WriteEndElement();
        }
        else
        {
            // It looks like the element doesn't have children, and we want to avoid emitting a self-closing tag.
            // First, let's temporarily disable any indenting, then force the full closing element tag.
            var prevFormat = this.Formatting;
            this.Formatting = Formatting.None;
            base.WriteFullEndElement();
            this.Formatting = prevFormat;
            this.inElement = false;
        }
    }
}
pbar
  • 451
  • 4
  • 5
1

Following code snippet force printing of closing tag on the same line (sorry for vb version, it should be easy to rewrite the same using C#):

Imports System.Xml
Imports System.IO
Public Class CustomXmlTextWriter
    Inherits XmlTextWriter

    Public Sub New(ByRef baseWriter As TextWriter)
        MyBase.New(baseWriter)
        Formatting = Xml.Formatting.Indented
    End Sub

    Public Overrides Sub WriteEndElement()
        If Not (Me.WriteState = Xml.WriteState.Element) Then
            MyBase.WriteEndElement()
        Else
            Formatting = Xml.Formatting.None
            MyBase.WriteFullEndElement()
            Formatting = Xml.Formatting.Indented
        End If
    End Sub

End Class
0

Another option.

public class XmlCustomTextWriter : XmlTextWriter
{
    private TextWriter _tw = null;

    public XmlCustomTextWriter(TextWriter sink)
        : base(sink)
    {
        _tw = sink;
        Formatting = Formatting.Indented;
        Indentation = 0;
    }

    public void OutputElement(string name, string value)
    {
        WriteStartElement(name);
        string nl = _tw.NewLine;
        _tw.NewLine = "";
        WriteString(value);
        WriteFullEndElement();
        _tw.NewLine = nl;
    }
}
Peter
  • 1
0

Leaving this here in case someone needs it; since none of the answers above solved it for me, or seemed like overkill.

    FileStream fs = new FileStream("file.xml", FileMode.Create);
    XmlWriterSettings settings = new XmlWriterSettings();
    settings.Indent = true;
    XmlWriter w = XmlWriter.Create(fs, settings);
    w.WriteStartDocument();
    w.WriteStartElement("tag1");

        w.WriteStartElement("tag2");
        w.WriteAttributeString("attr1", "val1");
        w.WriteAttributeString("attr2", "val2");
        w.WriteFullEndElement();

    w.WriteEndElement();
    w.WriteEndDocument();
    w.Flush();
    fs.Close();

The trick was to set the XmlWriterSettings.Indent = true and add it to the XmlWriter.

Edit:

Alternatively you can also use

w.Formatting = Formatting.Indented;

instead of adding an XmlWriterSettings.

PhoenixDev
  • 746
  • 2
  • 9
  • 22