Using Events to Signal Changes in Objects with C#

C/C++ Users Journal August, 2005

Some solutions scale upwards, others don't

By Grant Miller

Grant Miller is a software engineer at Miller Technologies. He can be contacted at grantmiller@iafrica.com.

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

  1. The first step is to define the information that needs to be passed when this event occurs. This is achieved by creating a class that inherits from the EventArgs class, and has a public access modifier. The naming convention is to use the event name and append EventArgs; lines 1-11 in Listing 1 are a typical implementation.
  2. The second step is to define a delegate, which is equivalent to a type-safe function pointer or callback. The first argument should be of type Object; this lets the event handlers (receivers) determine which object invoked the event. The second argument is of the type defined in the previous step. The delegate definition is used to define both events and event handlers (receivers). This typically uses a public access modifier. The naming convention uses the event name and appends EventHandler; see lines 13-14 in Listing 1.
  3. The third step is to define the event, whose type is the delegate defined in step 2. This definition is almost like a normal method definition. Looking at Listing 1, line 16, you see the public access modifier followed by the event keyword, then the data type, LabelChangeEventHandler, and finally the name of the event, LabelChange.
  4. The fourth step is to define a method that raises the event in the class. This is primarily used to check whether there are any registered event handlers before raising the event. It is usually declared with a protected access modifier. The naming convention uses the name of the event prefixed with the word On. Listing 1, lines 18-25 show this method declaration. When you want to raise the event in the class, you call this method. This is a good idea from a design and maintenance point of view because there is only one place where the event is raised. This makes it much easier to monitor when the event is raised, and easier to debug.

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 was—as 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 value—if 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:

Conclusion

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 significant—the 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.

Acknowledgments

Thanks to Heather and Trevor Main as well as Professor Wells for the late nights.

References

Richter, Jeffrey. Applied Microsoft .NET Framework Programming, Wintellect, 2002.