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.
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); } }
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: estdir
and a
subdirectory under C: estdir
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: estdir DirectoryInfoNotify dirInfo = new DirectoryInfoNotify(@"c: estdir"); // Register the notification object under both observers observer1.Register(dirInfo); observer2.Register(dirInfo); // Create the directory c: estdir 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: estdir Notified after creation of directory--sender: c: estdir Notified after creation of SUB-directory--sender: c: estdir Notified after creation of SUB-directory--sender: c: estdir Notified of directory deletion--sender: c: estdir ew Notified of directory deletion--sender: c: estdir Notified of directory deletion--sender: c: estdir
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: estdir"); // Register the DirectoryInfoNotify object with the DirectoryInfoObserver object observer.Register(dirInfo); // Create the c: estdir 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:
When the dirInfo.Create
method is called
When the dirInfo.CreateSubdirectory
method is
called
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
Xxx
Listener
methods are
called whenever their respective event is raised on a registered
DirectoryInfoNotify
object. Within these
Xxx
Listener
methods,
you can place any code that you wish to execute whenever a particular
event is raised.
These Xxx
Listener
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
Xxx
Listener
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
Xxx
Listener
method on
the observer object and then back to the object that raised the
event.
See Recipe 7.6; see the “Event” keyword and “Handling and Raising Events” topic in the MSDN documentation.