DekGenius.com
[ Team LiB ] Previous Section Next Section

Recipe 7.5 Adding Events to a Sealed Class

Problem

Through the use of inheritance, adding events to a nonsealed class is fairly easy. For example, inheritance is used to add events to a Hashtable object. However, adding events to a sealed class, such as System.IO.DirectoryInfo, requires a technique other than inheritance.

Solution

To add events to a sealed class, such as the DirectoryInfo class, wrap it using another class, such as the DirectoryInfoNotify class defined in the next example.

You can use the FileSystemWatcher class (see Recipe 11.23 and Recipe 11.24) to monitor the filesystem changes asynchronously due to activity outside of your program or you could use the DirectoryInfoNotify class defined here to monitor your program's activity when using the filesystem.


using System;
using System.IO;

public class DirectoryInfoNotify
{
    public DirectoryInfoNotify(string path)
    {
        internalDirInfo = new DirectoryInfo(path);
    }

    private DirectoryInfo internalDirInfo = null;
    public event EventHandler AfterCreate;
    public event EventHandler AfterCreateSubDir;
    public event EventHandler AfterDelete;
    public event EventHandler AfterMoveTo;

    protected virtual void OnAfterCreate( )
    {
        if (AfterCreate != null)
        {
            AfterCreate(this, new EventArgs( ));
        }
    }

    protected virtual void OnAfterCreateSubDir( )
    {
        if (AfterCreateSubDir != null)
        {
            AfterCreateSubDir(this, new EventArgs( ));
        }
    }

    protected virtual void OnAfterDelete( )
    {
        if (AfterDelete != null)
        {
            AfterDelete(this, new EventArgs( ));
        }
    }

    protected virtual void OnAfterMoveTo( )
    {
        if (AfterMoveTo != null)
        {
            AfterMoveTo(this, new EventArgs( ));
        }
    }

    // Event firing members
    public void Create( )
    {
        internalDirInfo.Create( );
        OnAfterCreate( );
    }

    public DirectoryInfoNotify CreateSubdirectory(string path)
    {
        DirectoryInfo subDirInfo = internalDirInfo.CreateSubdirectory(path);
        OnAfterCreateSubDir( );

        return (new DirectoryInfoNotify(subDirInfo.FullName));
    }

    public void Delete(bool recursive)
    {
        internalDirInfo.Delete(recursive);
        OnAfterDelete( );
    }

    public void Delete( )
    {
        internalDirInfo.Delete( );
        OnAfterDelete( );
    }

    public void MoveTo(string destDirName)
    {
        internalDirInfo.MoveTo(destDirName);
        OnAfterMoveTo( );
    }

    // Non-Event firing members
    public string FullName 
    {
        get {return (internalDirInfo.FullName);}
    }
    public string Name 
    {
        get {return (internalDirInfo.Name);}
    }
    public DirectoryInfoNotify Parent 
    {
        get {return (new DirectoryInfoNotify(internalDirInfo.Parent.FullName));}
    }
    public DirectoryInfoNotify Root 
    {
        get {return (new DirectoryInfoNotify(internalDirInfo.Root.FullName));}
    }

    public override string ToString( )
    {
        return (internalDirInfo.ToString( ));
    }
}

The DirectoryInfoObserver class, shown in the following code, allows you to register any DirectoryInfoNotify objects with it. This registration process allows the DirectoryInfoObserver class to listen for any events to be raised in the registered DirectoryInfoNotify object(s). The only events that are raised by the DirectoryInfoNotify object are after a modification has been made to the directory structure using a DirectoryInfoNotify object that has been registered with a DirectoryInfoObserver object:

public class DirectoryInfoObserver
{
    public DirectoryInfoObserver( ) {}

    public void Register(DirectoryInfoNotify dirInfo)
    {
        dirInfo.AfterCreate += new EventHandler(AfterCreateListener);
        dirInfo.AfterCreateSubDir += 
                new EventHandler(AfterCreateSubDirListener);
        dirInfo.AfterMoveTo += new EventHandler(AfterMoveToListener);
        dirInfo.AfterDelete += new EventHandler(AfterDeleteListener);
    }

    public void UnRegister(DirectoryInfoNotify dirInfo)
    {
        dirInfo.AfterCreate -= new EventHandler(AfterCreateListener);
        dirInfo.AfterCreateSubDir -= 
                new EventHandler(AfterCreateSubDirListener);
        dirInfo.AfterMoveTo -= new EventHandler(AfterMoveToListener);
        dirInfo.AfterDelete -= new EventHandler(AfterDeleteListener);
    }

    public void AfterCreateListener(object sender, EventArgs e)
    {
        Console.WriteLine("Notified after creation of directory--sender: " + 
                           ((DirectoryInfoNotify)sender).FullName);
    }

    public void AfterCreateSubDirListener(object sender, EventArgs e)
    {
        Console.WriteLine("Notified after creation of SUB-directory--sender: " + 
                           ((DirectoryInfoNotify)sender).FullName);
    }

    public void AfterMoveToListener(object sender, EventArgs e)
    {
        Console.WriteLine("Notified of directory move--sender: " + 
                           ((DirectoryInfoNotify)sender).FullName);
    }

    public void AfterDeleteListener(object sender, EventArgs e)
    {
        Console.WriteLine("Notified of directory deletion--sender: " + 
                           ((DirectoryInfoNotify)sender).FullName);
    }
}

Discussion

There are situations in which this technique might be useful even when a class is not sealed. For example, if you want to raise notifications when methods that have not been declared as virtual are called, you'll need this technique. So even if DirectoryInfo were not sealed, you would still need this technique because you can't override its Delete, Create, and other methods. And hiding them with the new keyword is unreliable because someone might use your object through a reference of type DirectoryInfo instead of type DirectoryInfoNotify, in which case they'll end up using the original methods and not your new methods. So the delegation approach presented here is the only reliable technique when methods in the base class are nonvirtual, regardless of whether the base class is sealed.

The following code creates two DirectoryInfoObserver objects along with two DirectoryInfoNotify objects, and then it proceeds to create a directory C:\testdir and a subdirectory under C:\testdir called new:

public void TestDirectoryInfoObserver( )
{
    // Create two observer objects
    DirectoryInfoObserver observer1 = new DirectoryInfoObserver( );
    DirectoryInfoObserver observer2 = new DirectoryInfoObserver( );

    // Create a notification object for the directory c:\testdir
    DirectoryInfoNotify dirInfo = new DirectoryInfoNotify(@"c:\testdir");

    // Register the notification object under both observers
    observer1.Register(dirInfo);
    observer2.Register(dirInfo);

    // Create the directory c:\testdir
    dirInfo.Create( );

    // Change the first observer to watch the new subdirectory
    DirectoryInfoNotify subDirInfo = dirInfo.CreateSubdirectory("new");
    observer1.Register(subDirInfo);

    // Delete the subdirectory first and then the parent directory
    subDirInfo.Delete(true);
    dirInfo.Delete(false);

    // Unregister notification objects with their observers
    observer2.UnRegister(dirInfo);
    observer1.UnRegister(dirInfo);
}

This code outputs the following:

Notified after creation of directory--sender: c:\testdir 
Notified after creation of directory--sender: c:\testdir
Notified after creation of SUB-directory--sender: c:\testdir
Notified after creation of SUB-directory--sender: c:\testdir
Notified of directory deletion--sender: c:\testdir\new
Notified of directory deletion--sender: c:\testdir
Notified of directory deletion--sender: c:\testdir

Rather than using inheritance to override members of a sealed class (i.e., the DirectoryInfo class), the sealed class is wrapped by a notification class (i.e., the DirectoryInfoNotify class).

The main drawback to wrapping a sealed class is that each method available in the underlying DirectoryInfo class might have to be implemented in the outer DirectoryInfoNotify class, which can be tedious if the underlying class has many visible members. The good news is that if you know you will not be using a subset of the wrapped class's members, you do not have to wrap each of those members. Simply do not make them visible from your outer class, which is what we have done in the DirectoryInfoNotify class. Only the methods we intend to use are implemented on the DirectoryInfoNotify class. If more methods on the DirectoryInfo class will later be used from the DirectoryInfoNotify class, they can be added with minimal effort.

For a DirectoryInfoNotify object to wrap a DirectoryInfo object, the DirectoryInfoNotify object must have an internal reference to the wrapped DirectoryInfo object. This reference is in the form of the internalDirInfo field. Essentially, this field allows all wrapped methods to forward their calls to the underlying DirectoryInfo object. For example, the Delete method of a DirectoryInfoNotify object forwards its call to the underlying DirectoryInfo object as follows:

public void Delete( )
{
    // Forward the call
    internalDirInfo.Delete( );

    // Raise an event
    OnAfterDelete( );
}

You should make sure that the method signatures are the same on the outer class as they are on the wrapped class. This convention will make it much more intuitive and transparent for another developer to use.

There is one method, CreateSubdirectory, that requires further explanation:

public DirectoryInfoNotify CreateSubdirectory(string path)
{
    DirectoryInfo subDirInfo = internalDirInfo.CreateSubdirectory(path);
    OnAfterCreateSubDir( );

    return (new DirectoryInfoNotify(subDirInfo.FullName));
 }

This method is unique since it returns a DirectoryInfo object in the wrapped class. However, if we also returned a DirectoryInfo object from this outer method, we might confuse the developer attempting to use the DirectoryInfoNotify class. If a developer is using the DirectoryInfoNotify class, he or she will expect that class to also return objects of the same type from the appropriate members rather than returning the type of the wrapped class.

To fix this problem and make the DirectoryInfoNotify class more consistent, a DirectoryInfoNotify object is returned from the CreateSubdirectory method. The code that receives this DirectoryInfoNotify object might then register it with any available DirectoryInfoObserver object(s). This technique is shown here:

// Create a DirectoryInfoObserver object and a DirectoryInfoNotify object
DirectoryInfoObserver observer = new DirectoryInfoObserver( );
DirectoryInfoNotify dirInfo = new DirectoryInfoNotify(@"c:\testdir");

// Register the DirectoryInfoNotify object with the DirectoryInfoObserver object
observer.Register(dirInfo);

// Create the c:\testdir directory and then create a sub directory within that 
//   directory this will return a new DirectoryInfoNotify object, which is 
//   registered with the same DirectoryInfoObserver object as the dirInfo object
dirInfo.Create( );
DirectoryInfoNotify subDirInfo = dirInfo.CreateSubdirectory("new");
observer.Register(subDirInfo);

// Delete this subdirectory
subDirInfo.Delete(true);

// Clean up
observer.UnRegister(dirInfo);

The observer object will be notified of the following events in this order:

  1. When the dirInfo.Create method is called

  2. When the dirInfo.CreateSubdirectory method is called

  3. When the subDirInfo.Delete method is called

If the second observer.Register method were not called, the third event (subDirInfo.Delete) would not be caught by the observer object.

The DirectoryInfoObserver class contains methods that listen for events on any DirectoryInfoNotify objects that are registered with it. The XxxListener methods are called whenever their respective event is raised on a registered DirectoryInfoNotify object. Within these XxxListener methods, you can place any code that you wish to execute whenever a particular event is raised.

These XxxListener methods accept a sender object parameter, which is a reference to the DirectoryInfoNotify object that raised the event. This sender object can be cast to a DirectoryInfoNotify object and its members may be called if needed. This parameter allows you to gather information and take action based on the object that raised the event.

The second parameter to the XxxListener methods is of type EventArgs, which is a rather useless class for our purposes. Recipe 7.6 shows a way to use a class derived from the EventArgs class to pass information from the object that raised the event to the XxxListener method on the observer object and then back to the object that raised the event.

See Also

See Recipe 7.6; see the "Event" keyword and "Handling and Raising Events" topic in the MSDN documentation.

    [ Team LiB ] Previous Section Next Section