Extending the XML DOM

Although the .NET Framework provides a suite of rich classes to navigate, query, and modify the contents of an XML document, there might be situations in which you need more functionality. For example, you might want a node class with more informative properties or a document class with extra functions. To obtain that class, you simply derive a new class from XmlNode, XmlDocument, or whatever XML DOM class you want to override. Let’s see how.

Custom Node Classes

As a general rule of thumb, you should avoid deriving node classes from the base class XmlNode. If necessary, derive node classes from a specialized and concrete node class like XmlElement or XmlAttribute. This will ensure that no key behavior of the node is lost in your implementation. But what kind of extensions can you reasonably build for a node?

I haven’t encountered any huge flaws in the design of the XML DOM node classes, so if you need extensions, it’s probably because you want to give nodes new methods or properties that simplify a particular operation you carry out quite often.

The Microsoft Developer Network (MSDN) documentation already provides an example of XML DOM extensions that adds line information to each node and then counts the number of element nodes a given document contains. (See the section “Further Reading,” on page 244, for more information about this example.) As mentioned, the ChildNodes property of the XmlDocument class does not cache the number of elements in the list. As a result, whenever you need to know the number of children and call the Count property, the entire list of nodes is walked from top to bottom. In addition, you have no way to distinguish between element nodes and leaf nodes.

In the MSDN documentation, you’ll find a class that attempts to solve this problem by extending the XmlDocument class with a custom GetCount method, shown here:

class LineInfoDocument : XmlDocument 
{
    ...
    public int GetCount() 
    {
        return elementCount;
    }
    ...
}

In the remainder of this section, however, we’ll look at a more substantial improvement to the XmlDocument class. In particular, you’ll learn how to build a kind of “sensitive” XML DOM that can detect any changes to the underlying disk file and automatically reload the new contents.

Building a Hot-Plugging XML DOM

Being able to detect changes to files and folders as they occur is a feature that many developers would welcome. Win32 provides a set of functions to get notifications of incoming changes to the size, the contents, or the attributes of a given file or folder. Unfortunately, the feature is limited to notifying registered applications that a certain event occurred in the watched file or folder but provides no further information about what happened to which file or folder and why.

To clarify, this feature was introduced with Microsoft Windows 95 and was tailor-made for Windows Explorer. Have you ever noticed that when you have a Windows Explorer view open and you modify a file shown in that view, the Windows Explorer view automatically refreshes to show updated data? The trick behind this apparently magical behavior is that, just before a new folder view is opened, Windows Explorer registers a file notification object for the contents of that folder. When it gets a notification that something occurred to that folder’s contents, Windows Explorer simply refreshes the view to show the new contents, whatever that is.

Later, Microsoft introduced only for the Windows NT platform an even more sophisticated mechanism that not only notifies applications of the event but also provides information about the type of change that occurred and the file or files affected. This extended feature relies on Win32 API functions supported only on Windows NT platforms, starting with Windows NT 4.0.

The .NET Framework wraps all this functionality into the FileSystemWatcher class, available from the System.IO namespace. This class takes advantage of the Windows NT–based API and for this reason is not available with Microsoft Windows 98, Microsoft Windows Me, and older platforms.

Note

Because FileSystemWatcher is a wrapper for the Windows NT API, it works only on computers running Windows NT, Windows 2000, or Windows XP. But you could write a wrapper class using a less powerful Win32 API and have it work on all Win32 platforms.


An instance of the FileSystemWatcher class is at the foundation of the extended version of the XmlDocument class that we’ll build in the next section. The new class, named XmlHotDocument, is capable of detecting any changes that have occurred in the underlying file and automatically notifies the host application of these changes.

The XmlHotDocument Class Programming Interface

The XmlHotDocument class inherits from XmlDocument and provides a new event and a couple of new properties, as shown in the following code. In addition, it overrides one of the overloads of the Load method—the method overload that works on files. In general, however, nothing would really prevent you from extending the feature to also cover streams or text readers as long as those streams and readers are based on disk files.

public class XmlHotDocument : XmlDocument
{
    public XmlHotDocument() : base()
    {
        m_watcher = new FileSystemWatcher();
        HasChanges = false;
        EnableFileChanges = false;
    }
    ...
}

As you can see, the preceding code includes the class declaration and the constructor’s code. Upon initialization, the class creates an instance of the file system watcher and sets the new public properties—HasChanges and EnableFileChanges—to false. Table 5-6 summarizes what’s really new with the programming interface of the XmlHotDocument class.

Table 5-6. Programming Interface of the XmlHotDocument Class
Property or Event Description
EnableFileChanges Boolean property that you use to toggle on and off the watching system. If set to true, the application receives notifications for each change made to the file loaded in the DOM. Set to false by default.
HasChanges Boolean property that the class sets to true whenever there are changes in the underlying XML file that the application has not yet processed. Set to false by default; is reset when you call the Load method again.
UnderlyingDocumentChanged Represents an event that the class fires whenever a change is detected in the watched file.

In addition, the XmlHotDocument class has one private member—the reference to the FileSystemWatcher object used to monitor file system changes.

The Watching Mechanism

An instance of the FileSystemWatcher class is created in the class constructor but is not set to work until the caller application sets the EnableFileChanges property to true, as shown here:

public bool EnableFileChanges
{
    get { return m_watcher.EnableRaisingEvents; }
    set {
        if (value == true)
        {
            // Get the local path of the current file
            Uri u = new Uri(BaseURI);
            string filename = u.LocalPath;

            // Set the path to watch for
            FileInfo fi = new FileInfo(filename);
            m_watcher.Path = fi.DirectoryName; 
            m_watcher.Filter = filename;

            // Set hooks for writing changes
            m_watcher.NotifyFilter = NotifyFilters.LastWrite;
            m_watcher.Changed +=
                new FileSystemEventHandler(this.OnChanged);

            // Start getting notifications
            m_watcher.EnableRaisingEvents = true;
        }
        else
            m_watcher.EnableRaisingEvents = false;
    } 
}

EnableFileChanges is a read/write property that is responsible for setting up the watching system when set to true. The watching system consists of Path and Filter properties that you use to narrow the set of files and folders that must be watched for changes.

The Path property sets the folder to watch, while the Filter property restricts the number of files monitored in that folder. If you set the Filter property to an empty string, the entire contents of the folder will be watched; otherwise, only the files matching the filter string will be taken into account. In this case, we just need to monitor a single file, so we’ll set the Filter property to the name of the document used to populate the current XML DOM.

Note

When setting the Filter property, avoid using fully qualified path names. Internally, the FileSystemWatcher class will be concatenating the Path and Filter properties to obtain the fully qualified path to filter out files and folders involved in any file-system-level event caught.


The XmlDocument class stores the name of the document being processed in its BaseURI property. Although the BaseURI property is a string, it stores the file name as a URI. As a result, a file name such as c:data.xml is stored in the BaseURI property as file:///c:/data.xml. Note that in the .NET Framework, URIs are rendered through an ad hoc type—the Uri class. To obtain the local path from a URI, you must first create a new Uri object and query its LocalPath property, as shown here:

Uri u = new Uri(BaseURI);
string filename = u.LocalPath;

Why can’t we just use the file name in the URI form? To avoid the rather boring task of parsing the path string to extract the directory information, I use the FileInfo class and its handy DirectoryName property. Unfortunately, however, the FileInfo class can’t handle file names in the URI format. The following code will throw an exception if filename is a URI:

FileInfo fi = new FileInfo(filename);
m_watcher.Path = fi.DirectoryName; 
m_watcher.Filter = fi.Name;

To finalize the watcher setup, you also need to define the change events that will be detected and register a proper event handler for each of them. You set the NotifyFilter property with any bitwise combination of flags defined in the NotifyFilters enumeration. In particular, you can choose values to detect changes in the size, attributes, name, contents, date, and security settings of each watched file. The following code simply configures the watcher to control whether the monitored file has something new written to it. The LastWrite flag actually causes an event to fire whenever the timestamp of the file changes, irrespective of the contents that you might have written to the file. In other words, the event also fires if you simply open and save the file without entering any changes.

m_watcher.NotifyFilter = NotifyFilters.LastWrite;
m_watcher.Changed += new FileSystemEventHandler(this.OnChanged);

// Start getting notifications
m_watcher.EnableRaisingEvents = true;

The changes you can register to be detected are originated by four events: Changed, Created, Deleted, and Renamed. In this example, we are interested only in the changes that modify an existing file, so let’s handle only the Changed event, as shown here:

private void OnChanged(object source, FileSystemEventArgs e)
{
    HasChanges = true;
    if (UnderlyingDocumentChanged != null)
        UnderlyingDocumentChanged(this, EventArgs.Empty); 
}

Any file system event passes to the handlers a FileSystemEventArgs object that contains information about the event—for example, the name of the files involved and a description of the event that just occurred. The XmlHotDocument class processes the Changed event by simply setting the HasChanges property to true and bubbling the event up to the caller application. In the process, the original event is renamed to a class-specific event named UnderlyingDocumentChanged. In addition, no argument is passed because the client application using the XML DOM needs to know only that some changes have occurred to the underlying documents currently being processed.

After it is completely set up, the FileSystemWatcher class starts raising file system events only if you set its EnableRaisingEvents property to true. Changing the value of this property to false is the only way you have to stop the watcher from sending further events.

Note

When monitoring a file or a folder through a FileSystemWatcher class, don’t be surprised if you receive too many events and some events that are not strictly solicited. The class is a watchful observer of what happens at the file system level and correctly reports any change you registered for. Many operations that look like individual operations are actually implemented in several steps, each of which can cause an independent event. In addition, you might have software running in the background (for example, antivirus software) that performs disk operations that will be detected as well.


Using the XmlHotDocument Class

To take advantage of the new class in a client application, start by declaring and instantiating a variable of that type, as follows:

XmlHotDocument m_hotDocument = new XmlHotDocument();

Next you register an event handler for the UnderlyingDocumentChanged event and call the Load method to build the XML DOM. When you think you are ready to start receiving file system notifications, set the EnableFileChanges property to true, as shown here:

m_hotDocument.UnderlyingDocumentChanged +=
    new EventHandler(FileChanged); 
m_hotDocument.Load("data.xml"); 
m_hotDocument.EnableFileChanges = true;

Note that you can’t set EnableFileChanges to true before the XML DOM is built—that is, before the Load method has been called.

Registering a handler for the custom UnderlyingDocumentChanged event is not mandatory, but doing so gives your application an immediate notification about what happened. The value of the HasChanges property automatically indicates any underlying changes that the current XML DOM does not yet reflect, however. When you build an XML DOM, the HasChanges property is reset to false. Figure 5-6 shows the sample application immediately after startup.

Figure 5-6. A sample application making use of the XmlHotDocument class. No pending changes have been detected yet on the displayed XML file.


When another user, or another application, modifies the XML file that is being processed by the current instance of the XmlHotDocument object, an UnderlyingDocumentChanged event reaches the application. The sample program shown in Figure 5-6 handles the event using the following code:

void FileChanged(object sender, EventArgs e)
{
    UpdateUI();
}

The internal UpdateUI method simply refreshes the user interface, checking the state of the HasChanges property, as shown here:

if (m_hotDocument.HasChanges)
    PendingChanges.Text = "*** Pending changes ***";

Figure 5-7 shows the application when it detects a change.

Figure 5-7. The sample application detects changes in the underlying XML file and updates the user interface.


At this point, the user can reload the XML DOM using the Load method again, as shown in the following code. As mentioned, calling the Load method resets the status of the HasChanges property, resulting in an up-to-date user interface.

public override void Load(string filename)
{
    // Load the DOM the usual way
    base.Load(filename);

    // Reset pending changes
	HasChanges = false;
}

Figure 5-8 shows the application displaying the change.

Figure 5-8. Changes dynamically occurring in the XML document are now correctly reflected by the XML DOM used by the application.


A hot-plugging XML DOM is more than a made-to-measure example. It is a piece of code that you might find useful in all those circumstances in which you make use of extremely volatile XML documents.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.31.26