How to force Non-Self Closing tags for empty nodes when using XslCompiledTransform class


In .NET 2.0 framework when using System.Xml.Xsl.XslCompiledTransform class for transforming a document then we always get self closing tags for all the elements which have nothing inside their body. There is no direct way to tell it to use separate closing tags
e.g.
<BODY>
    <TABLE></TABLE>
    <BR></BR>
</BODY>

transforms as

<BODY>
    <TABLE/>
    <BR/>
</BODY>

This might cause problems when we are transforming a document for HTML processing as HTML doesn’t identify self closing tags properly.
So although <Table/> is a valid XML closed tag but HTML identifies it as starting of a table only which causes its output to be different than what we want.
This thing did not happen with System.Xml.Xsl.XslTransform class in .NET 1.1 framework. It gives separate closing tags for empty elements too

This problem is caused because xslcompiledtransform class uses self closing tags for all the elements irrespective of whether they are empty or not.

The resolution of the issue involves creating a class derived from XmlTextWriter and overriding few methods in it. Here are the steps:

  • Create a new class XmlHtmlWriter inherited from XmlTextWriter.
  • Override two functions WriteStartElement and WriteEndElement.
  • Create a list fullyClosedElements for storing names of all the tags which you want not to be self closed.
  • In WriteStartElement function record the tag which is being written and in WriteEndElement check whether we need to close this element using separate closing tag or not by checking whether it exists in fullyClosedElements list or not.
  • To use separate tag for closing call WriteFullEndElement function else call base.WriteEndElement.
  • Then use an instance of this XmlHtmlWriter class in your application for output of xsl transform.
  • Here is the code for reference.

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;

namespace XSLSelfClosing
{
    public class XmlHtmlWriter : XmlTextWriter
    {
        public XmlHtmlWriter(System.IO.Stream stream, Encoding en)
            : base(stream, en)
        {

            //Put all the elemnts for which you want self closing tags in this list.
            //Rest of the tags would be explicitely closed

            fullyClosedElements.AddRange(new string[] { "br", "hr" });
        }

        string openingElement = "";
        List<string> fullyClosedElements = new List<string>();

        public override void WriteEndElement()
        {
            if (fullyClosedElements.IndexOf(openingElement) < 0)
                WriteFullEndElement();
            else
                base.WriteEndElement();
        }

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


Comments (2)

  1. jacobcoens@hotmail.com says:

    Hi,

    Thank you for sharing this. By coincidence I encountered this problem just one day after your post.

    A couple of things though: first, your code does the opposite of what it says. It does not make the self-closed tags from the xsl output close fully, but only the br and hr-tags.

    Here’s my version:

    using System;

    using System.Collections.Generic;

    using System.Text;

    using System.Xml;

    namespace myNamespace

    {

     public class XmlHtmlWriter : XmlTextWriter

     {

       string openingElement = String.Empty;

       List<string> selfClosingElements = new List<string>();

       public XmlHtmlWriter(System.IO.Stream stream, Encoding en)

         : base(stream, en)

       {

         //// Put all the elements for which you want self closing tags in this list.

         //// Rest of the tags would be explicitely closed

         selfClosingElements.AddRange(new string[] { "area", "base", "basefont", "br", "hr", "input", "img", "link", "meta" });

       }

       public override void WriteEndElement()

       {

         if (!selfClosingElements.Contains(openingElement))

         {

           WriteFullEndElement();

         }

         else

         {

           base.WriteEndElement();

         }

       }

       public override void WriteStartElement(string prefix, string localName, string ns)

       {

         base.WriteStartElement(prefix, localName, ns);

         openingElement = localName;

       }

     }

    }

    Also, for other readers, here’s how to use the class:

    MemoryStream output = new MemoryStream();

    XmlHtmlWriter writer = new XmlHtmlWriter(output, Encoding.UTF8);

    writer.Formatting = Formatting.Indented;

    XslCompiledTransform xslTrans = new XslCompiledTransform(true);

    xslTrans.Load(@"C:stylesheet.xsl");

    xslTrans.Transform(xmlInput, writer);

    output.Seek(0, SeekOrigin.Begin);

    StreamReader reader = new StreamReader(output);

    string result = reader.ReadToEnd();

  2. Darton says:

    Many thanks, this was almost exactly what I needed to do. Almost 4 years later, and we still have the same issues. Why?

    Here's my version to render valid XHTML with a stylesheet specifying XML output:

    public class XHtmlWriter : XmlTextWriter

    {

    private string cElement;

    private Dictionary<string, bool> eCElements;

    private Dictionary<string, bool> sCElements;

    public XHtmlWriter(System.IO.Stream stream, Encoding en)

    : base(stream, en)

    {

    eCElements = new Dictionary<string, bool>();

    eCElements.Add("label", false);

    eCElements.Add("textarea", false);

    sCElements = new Dictionary<string, bool>();

    sCElements.Add("input", false);

    sCElements.Add("br", false);

    sCElements.Add("hr", false);

    sCElements.Add("img", false);

    sCElements.Add("link", false);

    sCElements.Add("meta", false);

    }

    public override void WriteEndElement()

    {

    if (eCElements.ContainsKey(cElement))

    base.WriteFullEndElement();

    else

    base.WriteEndElement();

    }

    public override void WriteFullEndElement()

    {

    if (sCElements.ContainsKey(cElement))

    base.WriteEndElement();

    else

    base.WriteFullEndElement();

    }

    public override void WriteStartElement(string prefix, string localName, string ns)

    {

    cElement = localName;

    base.WriteStartElement(prefix, localName, ns);

    }

    }