Introduction
This article will show you how to render an XML string or System.Xml.XmlDocument the same way Internet Explorer renders an XML file. On the surface, this seems like a simple task. Wrong! If you believe that this is a simple task, you would be surprised how wrong you would be (I was). The user control in this example is useful because a lot of modern technologies depend on XML. As such, the absence of a simple way to display intermediate or generated XML in a presentable format is not satisfactory. That's why I created this control - because I couldn't find one like this one anywhere! Ever wondered how to display a formatted XML string or XML document without the need for an XML file? Do you despair, like I did, because the WebBrowser control doesn't present a simple way to do this? If so, look no further - this article presents the answer you seek.
The following screenshot depicts the rendering of a sample XML file. The file is loaded into a string before rendering.

Background
I have been architecting and developing solutions for many years, and have benefited a lot from the internet. I can't tell you the number of times searching for something has saved me countless hours. There are so many cool articles and cool people writing them. This is why I decided to write my first article - to give back to a community that has given me a lot. Also, because, I spent a few hours searching for a way to display an XML string in a way similar to how Internet Explorer does it.
I am surprised that although a lot of modern technologies depend on XML, the WebBrowser control does not natively allow you to view XML the way Internet Explorer does (unless you are viewing an XML file). Having to rely on the presence of an XML file to view formatted XML is not at all desirable.
As with many development endeavors, hours (several days) of research went into this one.
My journey began by discovering an XSL file (referred to as DEFAULTSS.XSL) that was using the old unsupported XSL namespace "http://www.w3.org/TR/WD-xsl". Efforts to modernize this to XSLT 1.0 were, initially, not completely fruitful due to the lack of a simple way to select and render namespaces and CData sections.
Luckily, I remembered the old way of doing things (FreeThreadedDOMDocument and the good old XSLTemplate). The old XSL processor can still be used in .NET. That was the break I needed. After that, I made important (and challenging) changes to complete an XSLT 1.0 version of the DEFAULTSS.XSL file (used by Internet Explorer to render XML files). This version was originally converted by Steve Muench, and can be found here.
Since I first wrote this article, I've tried a couple of new ways to render XML. One includes using the SAXON parser (XSLTHtml20.xslt); the other way (which I believe is the best way) includes using the namespace axis and escaping CData nodes before passing to XSLT transformation (XmlToHtml10Basic.xslt).
Using the code
The code for this article consists of two projects, XmlRender and XmlRenderTestApp.
| Name |
Description |
| XmlRender |
This project contains a few static class methods:
RenderXmlToHtml.Render(XmlDocument xmlToRender,XmlRender.XmlBrowser.XslTransformType xslTransformType)
RenderXmlToHtml.Render(System.Xml.XmlDocument xmlToRender)
RenderXmlToHtml.Render(string xmlToRender)
It also contains the XmlBrowser control which extends the WebBrowser with three settings:
System.Xml.XmlDocument XmlDocument
string XmlText
XmlRender.XmlBrowser.XslTransformType
All of these properties are used to indicate to the browser that we would like to display formatted XML (and how it should be rendered).
|
| XmlRenderTestApp |
This project contains a web form with a dropdown to select XML files, radio buttons to indicate whether or not we want to use an XSL or XSLT transformation, and the XmlBrowser control. Each file is loaded into a string, and sets the XmlDocument property of the XmlBrowser. |
XmlRender code:
internal static class RenderXmlToHtml
{
#region Render(XmlDocument xmlToRender,
XmlBrowser.XslTransformType xslTransformType)
internal static string Render(XmlDocument xmlToRender,
XmlBrowser.XslTransformType xslTransformType)
{
if (xslTransformType == XmlBrowser.XslTransformType.XSL)
return Render(xmlToRender.OuterXml);
else if (xslTransformType == XmlBrowser.XslTransformType.XSLT10)
return Render(xmlToRender,XmlRender.Properties.Resources.XmlToHtml10);
else if (xslTransformType == XmlBrowser.XslTransformType.XSLT10RegExp)
return Render(xmlToRender,
XmlRender.Properties.Resources.XmlToHtml10Plus);
return string.Empty;
}
#endregion
#region Render(string xmlToRender)
internal static string Render(string xmlToRender)
{
XSLTemplate oXSLT;
FreeThreadedDOMDocument oStyleSheet;
IXSLProcessor oXSLTProc;
DOMDocument oXMLSource;
try
{
oXSLT = new XSLTemplate();
oStyleSheet = new FreeThreadedDOMDocument();
oXMLSource = new DOMDocument();
oStyleSheet.async = false;
oStyleSheet.loadXML(XmlRender.Properties.Resources.XMLToHTML);
oXSLT.stylesheet = oStyleSheet;
oXSLTProc = oXSLT.createProcessor();
oXMLSource.async = false;
oXMLSource.loadXML(xmlToRender);
oXSLTProc.input = oXMLSource;
oXSLTProc.transform();
return oXSLTProc.output.ToString();
}
catch (Exception e)
{
return e.Message;
}
finally
{
oXSLT = null;
oStyleSheet = null;
oXMLSource = null;
oXSLTProc = null;
}
}
#endregion
#region Render(XmlDocument xmlToRender)
internal static string Render(XmlDocument xmlToRender,string xsltDocument)
{
XslCompiledTransform xslCompiledTransform;
XmlReader xmlReader;
System.IO.StringReader stringReader;
StringBuilder stringBuilder;
XmlWriter xmlWriter;
XsltSettings xsltSettings;
try
{
xslCompiledTransform = new XslCompiledTransform(true);
stringReader = new System.IO.StringReader(xsltDocument);
xmlReader = XmlReader.Create(stringReader);
xsltSettings = new XsltSettings(true, true);
xslCompiledTransform.Load(xmlReader,xsltSettings,new XmlUrlResolver());
stringBuilder = new StringBuilder();
xmlWriter = XmlWriter.Create(stringBuilder);
XsltArgumentList a = new XsltArgumentList();
a.AddParam("xmlinput", string.Empty, xmlToRender.OuterXml);
xslCompiledTransform.Transform(xmlToRender, a, xmlWriter);
return stringBuilder.ToString();
}
catch (Exception e) { return e.Message; }
finally
{
xslCompiledTransform = null;
xmlReader = null;
stringReader = null;
stringBuilder = null;
xmlWriter = null;
xsltSettings = null;
}
}
#endregion
}
XmlRenderTestApp code:
public partial class Form1 : Form
{
string _xmlDirectory;
#region Constructor
public Form1()
{
InitializeComponent();
FileInfo fi = new FileInfo(Assembly.GetExecutingAssembly().FullName);
_xmlDirectory = fi.DirectoryName + @"\XML Files\";
foreach(FileInfo file in new DirectoryInfo(_xmlDirectory).GetFiles("*.*"))
{
comboBox1.Items.Add(file.Name);
}
}
#endregion
#region comboBox select
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
XmlDocument _xd;
try
{
_xd = new XmlDocument();
string fileName = _xmlDirectory + comboBox1.SelectedItem.ToString();
System.Uri _uri = new Uri(fileName);
_xd.Load(fileName);
xmlBrowser1.XmlDocument = _xd;
}
finally
{
_xd = null;
}
}
#endregion
#region RadioButtons
private void radioButton1_CheckedChanged(object sender, EventArgs e)
{
xmlBrowser1.XmlDocumentTransformType =
XmlRender.XmlBrowser.XslTransformType.XSL;
}
private void radioButton2_CheckedChanged(object sender, EventArgs e)
{
xmlBrowser1.XmlDocumentTransformType =
XmlRender.XmlBrowser.XslTransformType.XSLT10;
}
private void radioButton3_CheckedChanged(object sender, EventArgs e)
{
xmlBrowser1.XmlDocumentTransformType =
XmlRender.XmlBrowser.XslTransformType.XSLT10RegExp;
}
#endregion
}
XmlBrowser code:
public partial class XmlBrowser : WebBrowser
{
#region Types/Private Variables
public enum XslTransformType
{
XSL,
XSLT10,
XSLT10RegExp
}
private XmlDocument _xmlDocument;
private XslTransformType _xslTransformType;
#endregion
#region Constructor
public XmlBrowser()
{
InitializeComponent();
}
#endregion
#region Properties
#region XmlText
[Category("XmlData")]
[Description("Use this property to set the XmlText
for rendering in the webbrowser.")]
public string XmlText
{
set
{
if (value != string.Empty)
{
_xmlDocument = new XmlDocument();
_xmlDocument.LoadXml(value);
this.DocumentText =
RenderXmlToHtml.Render(_xmlDocument,_xslTransformType);
}
}
get
{
if (_xmlDocument == null)
return string.Empty;
else
return _xmlDocument.OuterXml;
}
}
#endregion
#region XmlDocument
[Category("XmlData")]
[Description("Use this property in your code to set the
System.Xml.XmlDocument for rendering in the webbrowser.")]
public XmlDocument XmlDocument
{
get
{
return _xmlDocument;
}
set
{
if (value != null)
{
_xmlDocument = value;
this.DocumentText =
RenderXmlToHtml.Render(_xmlDocument,_xslTransformType);
}
}
}
#endregion
#region XmlDocumentTransformType
[Category("XmlData")]
[Description("Use this property to specify to use either
the XSL or XSLT 1.0 compliant stylesheet for rendering.")]
public XslTransformType XmlDocumentTransformType
{
get
{
return _xslTransformType;
}
set
{
_xslTransformType = value;
}
}
#endregion
#endregion
}
Points of interest
I thought about listing the XmlToHtml10.xslt/XmlToHtml10Plus.xslt file(s) here, but they are pretty big. I will tell you about the highlights though:
- XmlToHtml10.xslt does use
MSXML2.FreeThreadedDOMDocument a little bit in the JavaScript (I initially thought about using Regular Expressions, but I didn't do this at first because I didn't want to spend a lot of time writing this article).
- XmlToHtmlPlus.xslt only uses Regular Expressions. It does not use
MSXML2.FreeThreadedDOMDocument. This is particularly good because I'm thinking that it might be more efficient.
- There is some clever use of XPath building in the XSLT to identify the current context node to the JavaScript function. (See the section on
CDATA handling.)
- For some interesting examples of XSLT recursion, take a look at the
xmlnsSelector template or the getXPath template. XSLT recursion is always fun.
- Last but not least, if you debug/show the output of XmlToHtml10.xslt/XmlToHtml10Plus.xslt in VS2005, it won't take advantage of the interesting bits I've added. You can only see this at runtime.
I actually was writing another article to demonstrate how easy it is to shape a DataSet with table data retrieved from any database by supplying an XSD at run-time to a generic DataSet. The lack of XML viewability caused me to get side-tracked and write this article first! You can find the article I am referring to here.
I hope you like this article. I really enjoyed thinking about the challenges it presented.
Here is a brief overview of the transformation files included in this project:
| Name |
XSLT Processor |
Uses Extensions |
Description |
| XMLToHTML.xsl |
XSL |
No |
Original Microsoft XSL |
| XMLToHTML10.xslt |
XSLT1.0 |
Yes |
Uses FreeThreadDomDocument/Regular Expressions with XSLT to get at namespace nodes and CDATA |
| XMLToHTML10Plus.xslt |
XSLT1.0 |
Yes |
Uses only Regular Expressions to get at CDATA and namespace nodes |
| XMLToHTML10Cdata.xslt |
XSLT1.0 |
Yes |
Uses Regular Expressions to get at CDATA, and namespace axis to get at namespaces |
| XMLToHTML10Basic.xslt |
XSLT1.0 |
No |
Uses escaping of CDATA and namespace axis |
| XMLToHtml20.xslt |
XSLT2.0 |
No |
No rendering of namespaces or CDATA |
A general point - the transformations that don't use JavaScript extensions perform better than the ones that do.
History
- 12-Mar-2008
Initial release.
- 13-Mar-2008
Fixed a rendering issue so that an XML string now displays exactly like Internet Explorer viewing an XML document. Added the XMLBrowser control with two properties: XmlDocument and XmlText. This is to encapsulate XML formatting to a reusable user control within the XmlRender class library.
- 17-Mar-2008
Added the XSLT equivalent for DEFAULTSS.XSL. Also, added some checkboxes to the test app to allow toggling between XSL/XSLT transformation.
- 18-Mar-2008
Modified form to automatically populate based on files in the $(TargetDir)\XML Files directory. Files are automatically copied to this directory when building the project (via build events). No need to set build options on each file (just copy them to the XML files directory in the XmlRenderTestApp project.
- 24-Mar-2008
Added the Regular Expression only version of the XSLT file (called XmlToHtml10Plus.xslt). Also, made a slight correction to the XmlToHtml10.xslt, for consistency.
- 26-Mar-2008
Minor fix to XmlToHtml10Plus.xslt and XmlToHtml10.xslt with regards to xmlns-like attributes.
- 04-Apr-2008
Added SAXON implementation of XML rendering - still a work in progress, but has really cool functionality.
- 07-Apr-2008
Large XML option using the Microsoft XSLT parser will not add namespaces and CDATA nodes. Very quick.
- 14-Apr-2008
Large XML option using the Microsoft XSLT parser will add namespaces and CDATA nodes. Very quick. Uses the namespace axis and escaping of CDATA so that it can be used in XSLT.
References