Recipe 9.5. Wrapping Sealed Classes to Add EventsProblemThrough 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. SolutionTo add events to a sealed class, such as the DirectoryInfo class, wrap it using another class, such as the DirectoryInfoNotify class defined in Example 9-3.
Example 9-3. Adding events to a sealed class
The DirectoryInfoObserver class, shown in Example 9-4, 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. Example 9-4. DirectoryInfoObserver class
DiscussionWrapping is a very useful technique with many different applications (proxies, facades, etc.). However, if you use it, all classes in your application have to use the wrapped version of the class, or your wrapper code will not execute for cases when the sealed class is used directly. In some situations 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 to wrap those methods and supply the notifications. 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 the original methods and not your new methods will be used. So the delegation approach presented here is the only reliable technique when methods in the base class are not virtual methods, regardless of whether the base class is sealed. The TestDirectoryInfoObserver method shown in Example 9-5 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. Example 9-5. TestDirectoryInfoObserver method
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 you have done in the DirectoryInfoNotify class. Only the methods you 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. You could also make it completely different to differentiate the wrapper from the contained class. The key is not to have it look very similar to the contained class but with slight differences, as that would be the most confusing for your consumers. 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 you also returned a DirectoryInfo object from this outer method, you might confuse the developer attempting to use the DirectoryInfoNotify class. If a developer is using the DirectoryInfoNotify class, 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 subdirectory 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:
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 your purposes. Recipe 9.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 AlsoSee Recipe 9.6; see the "Event Keyword" and "Handling and Raising Events" topics in the MSDN documentation. |