Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

XML Explorer

4.86/5 (45 votes)
11 Sep 2008CPOL5 min read 1   8.6K  
An extremely fast, lightweight XML file viewer.

Go to the CodePlex project site for the latest releases and source code.

XmlExplorer Window with the XPath Expression Results Window

Contents

Introduction

At work, I find myself constantly having to view XML files. Some of the files I work with are extremely large (150MB+). Internet Explorer and Firefox take forever to open these files. I like the way Visual Studio 2005 provides the ability to expand/collapse individual elements, collapse or expand the entire document, etc., but it doesn't handle large files well either. Even the extremely expensive Stylus Studio chokes on large files, and doesn't offer the features of VS2005.

I explored several Open-Source solutions, including the ones from CodeProject, but most of them were not fast enough. Most loaded the entire document into a tree, all at once.

This article discusses my solution to the problem. I ended up with a nice little utility that gets used extensively around the office. It is an extremely fast, lightweight XML file viewer. It supports multiple document tabs, with support for middle-clicking to close tabs. It can be handle extremely large XML files. It has been tested on files as big as 150MB. It allows fast viewing and exploration, copying of formatted XML data, and evaluation of XPath expressions.

Background

My original implementation was based on the .NET XmlDocument. It was fast enough, but I didn't need any of the edit capabilities it offers. A colleague of mine, Mark (Code6) Belles, was kind enough to point me in the direction of the XPathDocument. XPathDocuments are even faster, and use much less memory than the XmlDocument, at the expense of the modification functionality.

According to MSDN, XPathDocument "provides a fast, read-only, in-memory representation of an XML document using the XPath data model". Sounded good to me.

Using the code

I've included several different ways to use the TreeView.

The XPathNavigatorTreeView can be added to your own forms via the designer in VS. Then, in code, just set the Navigator property of the XPathNavigatorTreeView to a XPathNavigator you've already loaded, for example:

C#
// load the XPathDocument
XPathDocument document = new XPathDocument(_filename);

// set the Navigator property of the XPathNavigatorTreeView
this.xmlTreeView.Navigator = document.CreateNavigator();

The XmlExplorerTabPage can be programmatically added to any standard TabControl:

C#
// create a tab page
XmlExplorerTabPage tabPage = new XmlExplorerTabPage();

// add the tabpage to the tab control
this.tabControl.TabPages.Add(tabPage);

// instruct the tab to open the specified file.
tabPage.Open(filename);

The TabbedXmlExplorerWindow can be shown, in process, from your own applications.

C#
// create a new application window
TabbedXmlExplorerWindow window = new TabbedXmlExplorerWindow();

// instruct the window to open a specified file
window.Open(filename);

// show the window
window.Show();

The XmlExplorer sample project is a fully-functional, standalone Windows application you can use to quickly view even extremely large XML files.

Points of interest

XPathNavigatorTreeView

One point of interest for the XPathNavigatorTreeView is the way TreeNodes are loaded on-demand, instead of loading nodes for every element in the document at once. Each node gets added with an empty 'dummy' node, so it can be expanded. I then override the OnBeforeExpand method, remove the dummy node, and add nodes for any child XML elements. Each node has a property to track whether it's been expanded and loaded, so it only gets loaded once.

I then use the XPathNavigator of an expanded tree node to select the child XML elements, returning an XPathNodeIterator. I use the iterator to add TreeNodes for the child XML elements, cloning the navigator for each node to maintain a cursor for future expansion.

C#
// select the child nodes of the specified xml tree node
XPathNodeIterator iterator = 
  treeNode.Navigator.SelectChildren(XPathNodeType.All);

// create and add a node for each navigator
foreach (XPathNavigator navigator in iterator)
{
    XPathNavigatorTreeNode node = 
        new XPathNavigatorTreeNode(navigator.Clone());
    treeNodeCollection.Add(node);
}

XPathNavigatorTreeNode

The implementation of XPathNavigatorTreeNode is pretty straightforward. Here is how I construct the text for the node:

C#
/// <summary>
/// Returns the text used to display this XPathNavigatorTreeNode,
/// formatted using the XPathNavigator it represents.
/// </summary>
/// <returns />
public string GetDisplayText()
{
    if (_navigator == null)
        return string.Empty;

    StringBuilder builder = new StringBuilder();
    switch (_navigator.NodeType)
    {
        case XPathNodeType.Comment:
            // comments are easy, just append the value inside  tags
            builder.Append("");
            break;

        case XPathNodeType.Root:
        case XPathNodeType.Element:
            // append the start of the element
            builder.AppendFormat("<{0} ", _navigator.Name);

            // append any attributes
            if (_navigator.HasAttributes)
            {
                // clone the node's navigator (cursor),
                // so it doesn't lose it's position
                XPathNavigator attributeNavigator = _navigator.Clone();
                if (attributeNavigator.MoveToFirstAttribute())
                {
                    do
                    {
                        builder.AppendFormat("{0}=\"{1}\" ", 
                                attributeNavigator.Name, attributeNavigator.Value);
                    }
                    while (attributeNavigator.MoveToNextAttribute());
                }
            }

            // if the element has no children, close the node immediately
            if (!_navigator.HasChildren)
            {
                builder.Append("/>");
            }
            else
            {
                // otherwise, an end tag node will be appended
                // by the XPathNavigatorTreeView after it's expanded
                builder.Append(">");
            }
            break;

        default:
            // all other node types are easy, just append the value
            // strings, whitespace, etc.
            builder.Append(this.StripNonPrintableChars(_navigator.Value));
            break;
    }
    return builder.ToString();
}

Loading XML files asynchronously

As fast as the XPathDocument can load large XML files, I still wanted the UI to remain responsive, and even allow the user to cancel the loading of a file. I implemented the methods used to load the files in an asynchronous manner, using the .NET Thread class.

C#
/// <summary>
/// Begins loading an XML file on a background thread.
/// </summary>
private void BeginLoadFile()
{
    _loadFileThread = new Thread(new ThreadStart(this.LoadFile));
    _loadFileThread.IsBackground = true;
    _loadFileThread.Start();
}

/// <summary>
/// The background worker method used to load an XML file in the background.
/// </summary>
private void LoadFile()
{
    try
    {
        if (this.LoadingFileStarted != null)
            this.LoadingFileStarted(this, EventArgs.Empty);

        Debug.WriteLine(string.Format("Peak RAM Before......{0}", 
              Process.GetCurrentProcess().PeakWorkingSet64.ToString()));
        Debug.Write("Loading XPathDocument.");
        DateTime start = DateTime.Now;

        // load the document
        XPathDocument document = new XPathDocument(_filename);

        Debug.WriteLine(string.Format("Done. Elapsed: {0}ms.", 
                        DateTime.Now.Subtract(start).TotalMilliseconds));

        // the UI has to be updated on the thread that created it,
        // so invoke back to the main UI thread.
        MethodInvoker del = delegate()
        {
            this.LoadDocument(document);
        };

        this.Invoke(del);

        if (this.LoadingFileCompleted != null)
            this.LoadingFileCompleted(this, EventArgs.Empty);
    }
    catch (ThreadAbortException ex)
    {
        // do not display the exception to the user,
        // as they most likely aborted the thread by
        // closing the tab or application themselves
        Debug.WriteLine(ex);
        if (this.LoadingFileFailed != null)
            this.LoadingFileFailed(this, EventArgs.Empty);
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex);

        MethodInvoker del = delegate()
        {
            MessageBox.Show(this, ex.ToString());
        };

        this.Invoke(del);

        if (this.LoadingFileFailed != null)
            this.LoadingFileFailed(this, EventArgs.Empty);
    }
}

/// <summary>
/// Loads an XPathDocument into the tree.
/// </summary>
private void LoadDocument(XPathDocument document)
{
    try
    {
        if (document == null)
            throw new ArgumentNullException("document");

        Debug.Write("Loading UI.");
        DateTime start = DateTime.Now;
        this.xmlTreeView.Navigator = document.CreateNavigator();
        Debug.WriteLine(string.Format("Done. Elapsed: {0}ms.", 
                        DateTime.Now.Subtract(start).TotalMilliseconds));
        Debug.WriteLine(string.Format("Peak RAM After......{0}", 
                        Process.GetCurrentProcess().PeakWorkingSet64.ToString()));
    }
    catch (ThreadAbortException)
    {
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex);
        MessageBox.Show(this, ex.ToString());
    }
}

Settings

The sample application also saves and restores user settings (such as window position, size, font, etc.) using the standard .NET Settings class. I used the excellent Visual Settings Designer in Visual Studio 2005 (just double-click the Properties node under your project, then go to Settings). I made all of the settings user-specific, so different users of the same machine can use different settings. The settings can be accessed and saved with the following code:

C#
// load the WindowState setting
window.WindowState = Properties.Settings.Default.WindowState;

...

// save the settings
Properties.Settings.Default.Save();

Performance

I've compared the times required to load XML files of varying sizes. XML Notepad was used for comparison, since it's nice enough to provide load times in the status bar. I realize the comparison isn't exactly fair, as XML Notepad is an XML editor. I trust you will find a place in your XML toolbox for both applications, as I have.

File SizeXML NotepadXML Explorer
47.4 MB48.68 seconds4.94 seconds
31.3 MB18.46 seconds3.63 seconds
30.96 MB16.47 seconds2.89 seconds
3.69 MB1.55 seconds.32 seconds
281 KB.35 seconds.04 seconds

As you can see, the difference is greatest on extremely large files. On the more normal sized files, the difference is barely noticeable.

Memory

XML Explorer also makes much more efficient use of RAM, by utilizing XPathDocument (instead of XmlDocument), and by loading the XML document into the display on-demand.

File SizeXML NotepadXML Explorer
47.4 MB563.3 MB RAM126.1 MB RAM
31.3 MB322.98 MB RAM73.92 MB RAM
3.69 MB51.87 MB RAM14.04 MB RAM
281 KB12.87 MB RAM6.85 MB RAM

Future enhancements

I would like to add the ability to edit, but it would likely come at the expense of speed and memory usage. I'm pretty happy using Visual Studio to edit XML files, and using XML Explorer for what it's best at. I may dedicate some time to adding edit capabilities in the future.

I will likely be adding XSL transformation in the near future.

Syntax highlighting and the ability to launch the XML editor of your choice were added in the 1.1 release.

The 2.0 release is now available, containing the following changes:

  • Visual Studio .NET style docking panes.
  • Expressions Library to save commonly-used XPaths.
  • Validation pane with support for XSD schema validation.
  • Document tabs are now full-featured panes, with support for tiling horizontally, vertically, etc.
  • Automatic update checking (only notifies when a new release is available).
  • About dialog (Help->About).
  • 'Recently Used Files' list under the File menu.

You can get it, and the code, over at the CodePlex project page. Still haven't had the time to update the article (or write a part 2).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)