C/C++ Users Journal August, 2005
Irecently faced a problem where I had a deeply nested hierarchy of GUI components, and users were able to access any of these levels and make alterations. The specific challenge was to know when users made changes and what had been changed, because these alterations needed to be saved.
Initially, I approached the problem by creating a property in each class of interest based on an enumerated type. Each property in the class that needed to be saved had a corresponding value in the enumeration. C# lets you treat enumerations as bit fields, so this let me easily determine which properties of the object had changed and what needed to be saved. This solution worked well enough for a small number of objects. However, problems started as the hierarchies grew and the time needed to parse the hierarchy rapidly increased. In short, this solution did not scale well.
Clearly I needed to change the approach that I was taking. Consequently, I wondered why the higher levels of the program parse the lower levels at all, and why don't the lower levels announce when they change?
One of the ideas that led to an answer to these questions is the concept of using events to notify changes. An event in C# is comparable to a C++ callback. Events, like callbacks, are used for asynchronous communication.
Before diving into specifics, I briefly outline the theory of defining an event in C# using Microsoft's recommended design pattern. The code I present (available at http://www.cuj.com/code/) usually resides in the class that raises the event. Listing 1 implements this approach
This covers the basic theory of event declaration and raising events. To clarify the jargon, an event is raised by a class and is consumed by registered event handlers.
In terms of using an event to notify a change, the basic idea is to define an event in each of the classes that would have been parsed for changes. When an object derived from one of these classes changes, an event is raised. Should there be any objects with registered event handlers, they will receive this event. If no objects have a registered event handler, then nothing happens. It is also possible to attach multiple event handlers to a single event. However, I have found from experience that the program's execution tends to slow with many handlers.
This is the approach I take in the remainder of this article. For purposes of illustration, I present a demonstration application; Figure 1 is its start-up screen. This program uses four buttons labeled Go! that should be clicked in a top-down consecutive fashion.
Listing 3 also shows the event-handler registration, but these are custom event handlers. There is a lot of casting happening in these examples, but the foundation of Listings 2 and 3 shows the method of event-handler registration. This procedure remains the same whether you are registering a method that handles a Windows event, or if you are registering a custom event handler. It really comes down to one line of code that uses the += operator. You may be wondering about unregistering event handlers. This is done in a similar fashion using the -= operator to remove an event handler. As you can see, it is easy to dynamically add and remove event handlers in C#.
After clicking the four Go! buttons, you can now drag any label to change its position, or right-click the label to change the caption. This functionality is provided by the event handlers registered for the standard Windows events (see Listing 2). When a change occurs, the panel containing the label changes its background color to red and the Save button is enabled. When a panel within a panel changes, the parent panel changes its background color to yellow, and the Save button is enabled. This functionality is made possible by registering label-change event handlers within the panel class.
To show that an event can be handled at any level of the hierarchy, I have registered event handlers at each level. C# can register both static and instance methods as event handlers. The only restriction regarding event handlers is that the object firing the event must be within its scope, which is entirely expected. In practice, I think it is better to handle the events at the least number of levels possible: ideally only one. When you run the sample application, make a few changes, then click the Save button to understand why.
For example, consider what happens when a SpecialLabel is dragged to the right and its X position property is altered. The SpecialLabel's OnChange method is called; this, in turn, raises the LabelChange event. The SpecialPanel in which this SpecialLabel resides then receives the event and executes its label_changed method. This method calls the OnChanged method, which raises the PanelChange event for the panel control. Because this panel is a child of the main SpecialPanel, the main SpecialPanel then receives this event and the panel_changed method executes. Similarly, this raises the PanelChange event that is handled by the test form in which it resides. The method panel_changed executes and stores the information that a change has been made to the main SpecialPanel. However, the child SpecialPanel has two event handlers registered with its PanelChange event, and only the first has executed. Now the second one, which resides in the test form, executes. Once again, the panel_changed method in the test form executes and the change is stored in a list. Similarly, there are two event handlers registered for every SpecialLabel control, one resides in the SpecialPanel class and the other in the test form. Now the test form's label_changed event will execute. This simply saves the SpecialLabel that changed, as well as what the change wasas disclosed in the argument of the event.
When the Save button is clicked, the program iterates the list of alterations by displaying each change in a message box. Each message box shows the name of the changed object, the type of change, and the property that was changed. For simplicity, the program uses an ArrayList to store objects of type SaveObject. However, this concept could easily be extended to filter superfluous changes. It could also be extended to reduce the casting necessary to extract objects by using a specialized list class.
The main components of this sample application are two classes, SpecialPanel and SpecialLabel. These classes are based on existing controls so that the example is not cluttered with unrelated information.
The class SpecialLabel inherits from the control System.Windows.Forms.Label. Listing 1 presents the code that relates to the event this class raises. This class defines an event called LabelChange that is invoked if the position or caption of the label changes. I've written wrapper properties for the properties of the base control in which I was interested. Most notably, the original Text property is wrapped by the Caption property. Similarly, the Left property is wrapped by the X property (see Listing 4). Also, the Top property of the original control is wrapped by the Y property. These new property definitions are enhanced to check for a change from the previous valueif it has changed, the LabelChange event is raised by calling the OnChange method.
The class SpecialPanel inherits from the control System.Windows.Forms.Panel. Apart from defining events for changes or additions (see PanelChange), this class has two functions of interest:
This solution means I no longer had to iterate through the hierarchy in search of changes. Although this is a sample application, there is a lot that can easily be extended for real development. For example, a program using this design can save changes as soon as an event notifying a change occurs, perhaps in a temporary file, and it can permanently save these changes when users click the Save button.
A possible disadvantage to using this approach is that using events does increase the memory overhead. The question is whether this additional memory overhead is significantthe answer is "no." As mentioned earlier, the delegate class is also responsible for registering event handlers, which it does by storing event handlers in a linked list, called a "delegate chain." The delegate chain basically stores pointers to the event handlers.
As an aside, the design methodology I used to define events should be altered when a class exposes more than one event. To get you started, the general idea is to use a hash table collection of event/delegate pairs.
Thanks to Heather and Trevor Main as well as Professor Wells for the late nights.
Richter, Jeffrey. Applied Microsoft .NET Framework Programming, Wintellect, 2002.