Operators, Delegates, and Events |
A complete PDF version of the text book is now available. The PDF version is an almost complete subset of the HTML version (where only a few, long program listings have been removed). See here. |
The event concept is central in event-driven programming. Programs with graphical user interfaces are event-driven. With the purpose of discussing events we will see a simple example of a graphical user interface at the end of this chapter.
|
23.1. Events
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
|
In command-driven programming, the computer prompts the user for input. When the user is prompted the program stops and waits a given program location. When a command is issued by the user, the program is continued at the mentioned location. The program will analyze the command and carry out an appropriate action.
In event-driven programming the program reacts on what happens on the elements of the user interface, or more generally, what happens on some selected state of the program. When a given event is triggered the actions that are associated with this particular event are carried out.
Inversion of control Don't call us - we call you |
The "Don't call us - we call you" idea is due to the observation that the operations called by the event mechanism is not activated explicitly by our own program. The operations triggered by events are called by the system, such as the graphical user interface framework. This is sometimes referred to as inversion of control.
Below, we compare operations (such as methods) and events.
|
In the following sections we will describe the event mechanism in C#. Fortunately, we have already made the preparations for this in Chapter 22, because an event can be modelled as a variable of a delegate type.
23.2. Events in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
In C# an event in some class C is a variable of a delegate type in C. Like classes, delegates are reference types. This implies that an event holds a reference to an instance of a delegate. The delegate is allocated on the heap.
From inside some class, an event is a variable of a delegate type. From outside a class, it is only possible to add to or remove from an event. Events are intended to provide notifications, typically in relation to graphical user interfaces. |
The following restrictions apply to events, compared to variables of delegate types:
|
In the System namespace there exists a generic delegate called EventHandler<TEVentArgs>, which is recommended for event handling in the .NET framework. Thus, instead of programming your own delegate types of your events, it is recommended to use a delegate constructed from EventHandler<TEVentArgs>. The EventHandler delegate takes two arguments: The object which generated the event and an object which describes the event as such. The latter is assumed to be a subclass of the pre-existing class EventArgs. For more information about EventHandler<TEVentArgs> consult the documentation of the generic EventHandler delegate. For details on generic delegates (type parameterized delegates) see Section 43.2.
23.3. Examples of events
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
In this section we will see examples of programs that make use of events.
First, in Program 23.1 we elaborate the Die class, which we have met several times before, see Section 10.1 , Section 12.5 , and Section 16.3.
In Program 23.1 the Toss operation of the Die class triggers a particular event in case it tosses two sixes in a row, see line 30-31.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | using System; using System.Collections.Generic; public delegate void Notifier(string message); public class Die { private int numberOfEyes; private Random randomNumberSupplier; private int maxNumberOfEyes; private List<int> history; public event Notifier twoSixesInARow; public int NumberOfEyes{ get {return numberOfEyes;} } public Die (): this(6){} public Die (int maxNumberOfEyes){ randomNumberSupplier = new Random(unchecked((int)DateTime.Now.Ticks)); this.maxNumberOfEyes = maxNumberOfEyes; numberOfEyes = randomNumberSupplier.Next(1, maxNumberOfEyes + 1); history = new List<int>(); history.Add(numberOfEyes); } public void Toss (){ numberOfEyes = randomNumberSupplier.Next(1,maxNumberOfEyes + 1); history.Add(numberOfEyes); if (DoWeHaveTwoSixesInARow(history)) twoSixesInARow("Two sixes in a row"); } private bool DoWeHaveTwoSixesInARow(List<int> history){ int histLength = history.Count; return histLength >= 2 && history[histLength-1] == 6 && history[histLength-2] == 6; } public override String ToString(){ return String.Format("Die[{0}]: {1}", maxNumberOfEyes, NumberOfEyes); } } | |||
|
In Program 23.1 Notifier is a delegate. Thus, Notifier is a type.
twoSixesInARow is an event - analogous to an instance variable - of type Notifier. Alternatively, we could have used the predefined EventHandler delegate (see Section 23.2) instead of Notifier. The event twoSixesInARow is public, and therefore we can add operations to this event from clients of Die objects. In line 9-11 of the class diceApp, shown in Program 23.2, we add an anonymous delegate to d1.twoSixesInARow, which reports the two sixes on the console.
Notice the keyword "event", used in declaration of variables of delegate types for event purposes. It is tempting to think of "event" as a modifier, which gives a slightly special semantics to a Notifier delegate. Technically in C#, however, event is not a modifier. The keyword event signals that we use a strictly controlled variable of delegate type. From outside the class, which contains the event, only addition and removal of methods/delegates are possible. The addition and removal can, inside the class, be controlled by so-called event accessors add and remove, which in several respect resemble get and set of properties. We will, however, not dwell on these features of C# in this material.
The predicate (boolean method) DoWeHaveTwoSixesInARow in line 35-40 of Program 23.1 in class Die determines if the die has shown two sixes in a row. This is based on the extra history instance variable.
Finally, the Toss operation may trigger the twoSixesInARow in line 31-32 of Program 23.1. The event is triggered in case the history tells that we have seen two sixes in a row.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System; class diceApp { public static void Main(){ Die d1 = new Die(); d1.twoSixesInARow += delegate (string mes){ Console.WriteLine(mes); }; for(int i = 1; i < 100; i++){ d1.Toss(); Console.WriteLine("{0}: {1}", i, d1.NumberOfEyes); } } } | |||
|
In Program 23.3 we show the (abbreviated) output of Program 23.2. The "two sixes in a row" reporting turns out to be reported in between the two sixes. This is because the event is triggered by Toss, before Toss returns the last 6 value.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 1: 6 2: 4 ... 32: 3 33: 6 Two sixes in a row 34: 6 Two sixes in a row 35: 6 ... 66: 2 67: 6 Two sixes in a row 68: 6 69: 2 70: 4 ... 97: 6 Two sixes in a row 98: 6 99: 3 | |||
|
We will now turn to a another example in an entirely different domain, see Program 23.4. This program constructs a graphical user interface with two buttons and a textbox, see Figure 23.1. If the user pushes the Click Me button, this is reported in the textbox. If the user pushes the Erase button, the text in the textbox is deleted.
Figure 23.1 A graphical user interface with two buttons and a textbox. |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | using System; using System.Windows.Forms; using System.Drawing; // In System: // public delegate void EventHandler (Object sender, EventArgs e) public class Window: Form{ private Button b1, b2; private TextBox tb; // Constructor public Window (){ this.Size=new Size(150,200); b1 = new Button(); b1.Text="Click Me"; b1.Size=new Size(100,25); b1.Location = new Point(25,25); b1.BackColor = Color.Yellow; b1.Click += ClickHandler; // Alternatively: // b1.Click+=new EventHandler(ClickHandler); b2 = new Button(); b2.Text="Erase"; b2.Size=new Size(100,25); b2.Location = new Point(25,55); b2.BackColor=Color.Green; b2.Click += EraseHandler; // Alternatively: // b2.Click+=new EventHandler(EraseHandler); tb = new TextBox(); tb.Location = new Point(25,100); tb.Size=new Size(100,25); tb.BackColor=Color.White; tb.ReadOnly=true; tb.RightToLeft=RightToLeft.Yes; this.Controls.Add(b1); this.Controls.Add(b2); this.Controls.Add(tb); } // Event handler: private void ClickHandler(object obj, EventArgs ea) { tb.Text = "You clicked me"; } // Event handler: private void EraseHandler(object obj, EventArgs ea) { tb.Text = ""; } } class ButtonTest{ public static void Main(){ Window win = new Window(); Application.Run(win); } } | |||
|
The program makes use of the already existing delegate type System.EventHandler. Operations in this delegate accept an Object and an EventArg parameter, and they return nothing (void).
The constructor of the class Window (which inherits from Form - a built-in class) dominates the program. In this constructor the window, aggregated by two buttons and a textbox, is built.
As emphasized in Program 23.4 we add handlers to the events b1.Click and b2.Click. We could have instantiated EventHandler explicitly, as shown in the comments, but the notion b1.Click += ClickHandler and b2.Click += EraseHandler is shorter and more elegant.
The two private instance methods ClickHandler and EraseHandler serve as event handlers. Notice that they conform to the signature of the EventHandler. (The signature is characterized by the parameter types and the return type).
Exercise 6.5. Additional Die events In this exercise we add yet another method to the existing event i class Die, and we add another event to Die. In the Die event example, we have a public event called twoSixesInARow which is triggered if a die shows two sixes in a row. In the sample client program we add an anonymous method to this event which reports the string parameter of the event on standard output. Add yet another method to the twoSixesInARow event which counts the number of times 'two sixes in a row' appear. For this purpose we need - quite naturally - an integer variable for counting. Where should this variable be located relative to the 'counting method': Will you place the variable inside the new method, inside the Die class, or inside the client class of the Die? Add a similar event called fullHouse, of the same type Notifier, which is triggered if the Die tosses a full house. A full house means (inspired from the rules of Yahtzee) two tosses of one kind and three tosses of another kind - in a row. For instance, the toss sequence 5 6 5 6 5 leads to a full house. Similarly, the 1 4 4 4 1 leads to a full house. The toss sequence 5 1 6 6 6 6 5 does not contain a full house sequence, and the toss sequence 6 6 6 6 6 is not a full house. Be sure to test-drive the program and watch for triggering of both events. Solution |