Introduction
Have you ever been in a situation where you wanted to display XML-data to your users and/or make it editable? To achieve this, one might first think of the (in .NET 1.0 unmanaged) Browser COM plug-in. It does the display part quite well, although the unnerving JavaScript-blocker on WinXP SP2 spoilt that a bit. Also, you might really require editing capabilities as well and that plug-in does not really help you that much, does it?! Also, when allowing your users to edit the XML-data it might be quite nice to get some sort of validation of what the user enters. Moreover, you can't be bothered writing all that printing code yourself when you can have a control that handles all of that, much like the Internet Explorer plug-in.
Well, hopefully, this control might help you a bit. Feel free to change it, extend it, customize it or simply use it in your application.
The XML TreeView offers the following built-in functionality:
- Printing of an XML document.
- Editing XML nodes.
- Deleting XML nodes.
- Searching an XML document.
- Validating XML data entered by the user.
- Saving XML to file.
- Copying and pasting XML data.
- Dragging and dropping XML files onto the control.
- Opening XML files through OpenFileDialog.
Using the component in your code
Simply download the Zip-file containing the binary, extract the assembly to your local hard drive, and install it in your Visual Studio .NET Toolbox. That's it! You're ready to use the component in your Windows Forms applications.
There are only two properties of interest when using the control in your code, i.e. the property "LabelEdit
" and the property "Xml
" of type XmlTreeView
. The value of the boolean property "LabelEdit
" decides whether the tree nodes can be edited and deleted whereas the string property "Xml
" allows you to programmatically retrieve and set the XML-data displayed by the control. Functionality can usually be accessed in two ways, that is through a pop-up menu which is contained in the XML-TreeView, or through keyboard action, e.g. Ctrl+P to trigger the printing of a document.
Getting to grips with the source code
First of all, we need a way to abstract from the System.Windows.Forms.TreeView
because we need to cater for error situations when it makes simply no sense to display the erroneous data in a tree structure. Therefore, we require a container control which makes the decision as to which type of child control is best suited to display the data loaded into the XML-control. This task is assumed by the type Tom.XmlControls.XmlTreeView
which derives from System.Windows.Forms.UserControl
and publishes the aforementioned properties LabelEdit
and Xml
to the outside world. This container creates an instance of Tom.XmlControls.InnerXmlTreeView
- after all this is a control to display XML, so this is the default-control - and attempts to assign the data to its Xml
-property. If this causes an exception to be thrown, the container will create an instance of Tom.XmlControls.InnerTextBox
and assign it the data. Obviously the latter will then not display it in a tree structure but rather as plain text. In order to bind the two types InnerXmlTreeView
and InnerTextBox
to a sort of implementation contract, they must implement the interface Tom.XmlControls.IXmlControl
so as to hide the fact from the user that controls are dynamically swapped in and out.
Below is the definition of this interface:
using System;
using System.Drawing.Printing;
namespace Tom.XmlControls
{
internal interface IXmlControl
{
#region Content
string Xml { get; set; }
#endregion
#region Searching
void Search( object sender, EventArgs e );
void StartSearch( string criterion, bool caseSensitive );
void Next();
#endregion
#region Printing
void Print( object sender, EventArgs e );
void PrintPage( object sender, PrintPageEventArgs e );
PrintDocument PrintDocument { get; }
#endregion
#region Opening, Editing, Deleting, Copying, Pasting
void Edit(object sender, System.EventArgs e);
void Delete(object sender, System.EventArgs e);
void Copy(object sender, System.EventArgs e);
void Paste(object sender, System.EventArgs e);
#endregion
#region Saving, Opening
void Save(object sender, System.EventArgs e);
void Open(object sender, System.EventArgs e);
#endregion
}
}
The two implementations of this interface are InnerXmlTreeView
and InnerTextBox
. Unfortunately, not all of the expected contract can be enforced by the interface. This holds especially true for inherited event handlers which have to be overridden such as those for handling drag/drop events:
protected override void OnDragEnter( DragEventArgs e )
{
base.OnDragEnter( e );
if ( e.Data.GetDataPresent( DataFormats.FileDrop ) )
{
e.Effect = DragDropEffects.Copy;
draggedFile = (string) ( (object[])
e.Data.GetData( DataFormats.FileDrop ) )[0];
}
}
protected override void OnDragDrop( DragEventArgs e )
{
base.OnDragDrop( e );
if ( draggedFile.Length > 0 )
{
using ( StreamReader fs = new StreamReader( draggedFile ) )
{
parent.Xml = fs.ReadToEnd();
}
draggedFile = string.Empty;
}
}
Assuming that we actually have valid XML-data (remember, we are not talking about schema-validation) which can be loaded into a DOM, we'll have to parse the DOM and build the tree structure of the TreeView accordingly. This is done by the following method which is called recursively:
private void RecurseAndAssignNodes( XmlElement elem )
{
string attrs = string.Empty;
XmlTreeNode addedNode = null;
foreach ( XmlAttribute attr in elem.Attributes )
{
attrs += " " + attr.Name + "=\"" + attr.Value + "\"";
}
if ( elem.Equals( document.DocumentElement ) )
{
addedNode = new XmlTreeNode( "<" + elem.Name + attrs + ">", elem );
Nodes.Add( addedNode );
InnerXmlTreeView.CurrentNode = addedNode;
Nodes.Add( new XmlTreeNode( "</" + elem.Name + ">", null ) );
}
else
{
if ( elem.HasChildNodes &&
elem.ChildNodes[0].NodeType == XmlNodeType.Text )
{
addedNode = new XmlTreeNode( "<" + elem.Name + attrs +
">" + elem.InnerText + "</" +
elem.Name + ">", elem );
InnerXmlTreeView.CurrentNode.Nodes.Add( addedNode );
InnerXmlTreeView.CurrentNode = addedNode;
}
else
{
if ( elem.IsEmpty )
{
addedNode = new XmlTreeNode( "<" +
elem.Name + attrs + "/>", elem );
InnerXmlTreeView.CurrentNode.Nodes.Add( addedNode );
InnerXmlTreeView.CurrentNode = addedNode;
}
else
{
addedNode = new XmlTreeNode( "<" + elem.Name + attrs + ">", elem );
InnerXmlTreeView.CurrentNode.Nodes.Add( addedNode );
InnerXmlTreeView.CurrentNode = addedNode;
InnerXmlTreeView.CurrentNode.Parent.Nodes.Add( new
XmlTreeNode( "</" + elem.Name + ">", null ) );
}
}
}
XmlElement nextElement = null;
foreach ( XmlNode child in elem.ChildNodes )
{
if ( ( nextElement = child as XmlElement ) != null )
{
RecurseAndAssignNodes( nextElement );
}
else if ( child.NodeType == XmlNodeType.Comment )
{
InnerXmlTreeView.CurrentNode.Nodes.Add( child.OuterXml );
}
}
if ( InnerXmlTreeView.CurrentNode.Parent != null )
{
InnerXmlTreeView.CurrentNode = InnerXmlTreeView.CurrentNode.Parent;
}
}
Points of Interest
Recursion is of vital importance in this context and a powerful technique to parse any tree-like structure such as XML, directory trees and so on. However, it bears its pitfalls especially when it comes to local method variables and parameters. But most of the time, storing information at instance level will remedy these problems. Nonetheless, recursion can have a severe performance impact and is generally discouraged, unless you're programming LISP, because it is equally annoying to debug.
History
- Released version 1.0.0.0 on 5th August 2005.
- Released Version 1.0.0.1 on 9th August 2005 [Bug fix: Edit-mode
InnerXmlTreeView
].
- Released Version 1.0.0.2 on 2nd September 2005 [Bug fix: XML-Comments].
- Released Version 1.0.0.3 on 23rd September 2005 [Bug fix: parent controls other than forms].
- Released Version 1.0.0.4 on 10th October 2005 [New public property
XmlTreeView.XmlFile
takes string path to a file containing XML-data].