Introduction
This article will show you how to render an XML string or System.Xml.XmlDocument
in 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 namespace
s 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