Delegaten sind nützlich, um von Objekten benachrichtigt zu werden, wenn bestimmte
Ereignisse eintreffen. Allerdings gibt es ein Problem, wenn sich ein beobachtendes
Objekt an einen Delegaten anhängen will - entweder müssen die Delegaten für den
Zugriff von außen freigegeben werden, oder es müssen entsprechende Methoden zum
Hinzufügen und Entfernen einer Methode existieren.
Beide Varianten verfügen jeweils über einige Nachteile. Während bei der Freigabe für
den Zugriff von außen die Kontrolle verloren geht, so dass beispielsweise sämtliche
gebundenen Methoden von außen entfernt werden könnten, erzeugt die Bereitstellung
entsprechender Methoden zusätzlichen Entwicklungs- und Wartungsaufwand.
Um den Zugriff sauber kapseln zu können und den Entwicklungsaufwand möglichst gering
zu halten, verfügt C# über das Konzept der Ereignisse. Prinzipiell ist ein Ereignis
nichts anderes als eine für interne Delegaten öffentlich verfügbare Schnittstelle,
über die beliebige Methoden an den zugehörigen Delegaten gebunden werden können.
Insofern fußen Ereignisse in C# auf der Basis der Delegaten.
Damit ein Ereignis definiert werden kann, muss zunächst ein entsprechender Delegat
bestehen, der als Vorlage für die Rückrufmethoden des Ereignisses fungiert. Delegaten,
die für Ereignisse eingesetzt werden, folgen einer anderen Namenskonvention als die
übrigen Delegaten: Ihr Name besteht aus dem Namen des Ereignisses in Pascal Case,
ergänzt um das Suffix EventHandler.
Die Ereignisse an sich werden mit Hilfe des Schlüsselwortes
event in der Klasse
ComplexNumber definiert, wobei der zu verwendende Delegat in der Definition
angegeben wird. Für Ereignisse gilt die Namenskonvention, dass ihr Name einem
Verb entspricht - in der Verlaufsform, falls das Ereignis ausgelöst wird, bevor
die eigentliche Aktion ausgeführt wird, in der Vergangenheitsform, falls
danach. Für die Schreibweise gilt Pascal Case.
In der Regel sollen Methoden, die durch ein Ereignis aufgerufen werden, einige
Informationen über das ursprüngliche, das Ereignis auslösende, Objekt erhalten. Daher
wird ein Delegat, der als ereignisbehandelnde Methode fungiert, selten parameterlos definiert.
Es gilt als guter Stil, zwei Parameter mitzugeben, von denen der erste eine Referenz auf
das Objekt, welches das Ereignis ausgelöst hat, zur Verfügung stellt, der zweite hingegen
zusätzliche Informationen zu dem Ereignis an sich enthält.
Für den ersten Parameter wird zumeist der Typ
object verwendet, wobei der Parameter mit
dem Namen sender versehen wird. Der Typ des zweiten Parameters entspricht häufig einer eigens zu
diesem Zweck definierten Klasse, die lediglich Felder und zugehörige Eigenschaften enthält,
um Daten auszutauschen, wobei diese Klasse üblicherweise von der Klasse EventArgs aus dem
Namensraum System abgeleitet wird.
Der Name der Klasse folgt den für Klassen üblichen Namenskonventionen, wobei als Suffix
zusätzlich noch EventArgs angehängt wird. Sofern keine eigene Klasse zum Datenaustausch
benötigt wird, kann auch direkt auf die Klasse EventArgs zurückgegriffen werden. Als
Name für den Parameter wird in beiden Fällen üblicherweise der Buchstabe e verwendet,
gängig sind allerdings auch ea, eventArgs und eventArguments.
Obwohl in der Framework Class Library durchgängig e verwendet wird, entspricht dies am
wenigsten den Namenskonventionen von C#. Unter diesem Gesichtspunkt sollte am ehesten
eventArguments eingesetzt werden.
| C# |
1
2
3
|
public delegate void Bar(object sender, EventArgs e);
public event Bar FooEvent;
|
Sofern ein Ereignis auf Grund einer Datenänderung auftritt, werden im ersten Parameter
häufig so wohl der alte wie auch der neue Wert an alle ereignisbehandelnden Methoden
übergeben. Diese haben dann die Möglichkeit, auf Basis dieser beiden Werte eigene Aktionen
auszuführen. Gelegentlich wird dieser Parameter zudem eingesetzt, um die Ausführung des
Ereignisses abzubrechen, indem eine entsprechende Eigenschaft namens Cancel auf
true
gesetzt wird, die schließlich vor der eigentlichen Ausführung des Ereignisses abgefragt
wird.
| C# |
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Executes when storing begins.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event arguments.</param>
public delegate void StoringEventHandler(
object sender, EventArgs eventArguments);
/// <summary>
/// Executes when storing has finished.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event arguments.</param>
public delegate void StoredEventHandler(
object sender, EventArgs eventArguments);
/// <summary>
/// Executes when restoring begins.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event arguments.</param>
public delegate void RestoringEventHandler(
object sender, EventArgs eventArguments);
/// <summary>
/// Executes when restoring has finished.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event arguments.</param>
public delegate void RestoredEventHandler(
object sender, EventArgs eventArguments);
/// <summary>
/// Represents a complex number.
/// </summary>
public sealed class ComplexNumber : IPersistable
{
#region Properties
#endregion
#region Events
/// <summary>
/// Fires when storing begins.
/// </summary>
public event StoringEventHandler Storing;
/// <summary>
/// Fires when storing has finished.
/// </summary>
public event StoredEventHandler Stored;
/// <summary>
/// Fires when restoring begins.
/// </summary>
public event RestoringEventHandler Restoring;
/// <summary>
/// Fires when restoring has finished.
/// </summary>
public event RestoredEventHandler Restored;
#endregion
#region Methods
// ...
/// <summary>
/// Stores the current instance in the specified
/// memento.
/// </summary>
/// <param name="memento">The memento.</param>
public void Store(IMemento memento)
{
// TODO gr: Store the current instance.
// 2007-06-25
}
/// <summary>
/// Restores the current instance from the specified
/// memento.
/// </summary>
/// <param name="memento">The memento.</param>
public void Restore(IMemento memento)
{
// TODO gr: Restore the current instance.
// 2007-06-25
}
#endregion
#region Constructors
#endregion
}
}
|
Obwohl die in diesem Beispiel verwendeten Ereignisse sämtlich objektgebunden sind, können
Ereignisse mit Hilfe des Schlüsselwortes
static wie auch Felder,
Eigenschaften und Methoden als klassengebunden definiert werden. Klassengebundene Ereignisse
erlauben es, auf Aktionen des gesamten Typs und nicht eines speziellen Objekts zu reagieren.
Nachdem ein Ereignis definiert wurde, kann es ausgelöst werden. Prinzipiell geschieht dies,
indem es wie eine Methode aufgerufen wird, wobei die gleichen Konventionen wie für den direkten
Aufruf einer Methode oder eines Delegaten gelten. Intern wird dabei der Aufruf an den
Delegaten weitergereicht, der dem Ereignis zugeordnet ist. Dieser wiederum löst - je nachdem,
ob es sich um einen Singlecast- oder einen Multicast-Delegaten handelt, eine oder mehrere
Methoden aus, die an den Delegaten gebunden worden sind.
Da ein Ereignis in der Regel von dem Objekt ausgelöst wird, das auch die Ursache für das
Ereignis darstellt, wird zumeist
this als erster Parameter angegeben. Der zweite Parameter
muss allerdings kontextbezogen erzeugt werden. Da sich dies aufwändiger gestalten kann,
wird das Auslösen eines Ereignisses in eine eigene Methode ausgelagert, deren Aufruf sich
an den entsprechenden Stellen dann deutlich kompakter als das direkte Auslösen des Ereignisses
gestaltet.
Als Name trägt eine solche Methode den Namen des Ereignisses, ergänzt um das Präfix On. Die
Methode, die also beispielsweise das Ereignis Stored auslöst, hieße OnStored. Häufig werden
in der Praxis die Methoden, die auf ein Ereignis reagieren, derart benannt, was nach den
Namensrichtlinien von C# allerdings falsch ist.
Da diese Methoden nur von innerhalb der Klasse ausgelöst werden sollten, werden sie in der
Regel mit dem Zugriffsmodifizierer
protected und zusätzlich mit dem Schlüsselwort
virtual versehen. Dies geschieht, damit eine abgeleitete Klasse
die ereignisauslösende Methode gegebenenfalls überschreiben kann. Im folgenden Beispiel ist
die Klasse allerdings versiegelt, weshalb der Zugriffsmodifizierer
private gewählt wurde.
Bevor ein Ereignis in einer solchen Methode aufgerufen wird, sollte zunächst noch geprüft
werden, ob sich überhaupt Objekte zur Überwachung des Ereignisses registriert haben. Da der
Delegat ansonsten
null ist, würde der Aufruf ohne eine solche Prüfung ins Leere laufen und
einen Fehler erzeugen, der zum Abbruch der Anwendung führt.
Obwohl noch nicht alle Konzepte vorgestellt wurden, die für diese Prüfung benötigt werden,
wird sie an dieser Stelle dennoch eingeführt, da sie zum einen zwingend benötigt wird, zum
anderen Ereignisaufrufe sich nur durch das konkrete, auszulösende Ereignis unterscheiden -
der Rest folgt immer dem gleichen Schema. Nähere Informationen finden sich in den Kapiteln
zu Operatoren und Anweisungen.
| C# |
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
using System;
namespace GoloRoden.GuideToCSharp
{
/// <summary>
/// Executes when storing begins.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event arguments.</param>
public delegate void StoringEventHandler(
object sender, EventArgs eventArguments);
/// <summary>
/// Executes when storing has finished.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event arguments.</param>
public delegate void StoredEventHandler(
object sender, EventArgs eventArguments);
/// <summary>
/// Executes when restoring begins.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event arguments.</param>
public delegate void RestoringEventHandler(
object sender, EventArgs eventArguments);
/// <summary>
/// Executes when restoring has finished.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event arguments.</param>
public delegate void RestoredEventHandler(
object sender, EventArgs eventArguments);
/// <summary>
/// Represents a complex number.
/// </summary>
public sealed class ComplexNumber : IPersistable
{
#region Properties
#endregion
#region Events
/// <summary>
/// Fires when storing begins.
/// </summary>
public event StoringEventHandler Storing;
/// <summary>
/// Fires when storing has finished.
/// </summary>
public event StoredEventHandler Stored;
/// <summary>
/// Fires when restoring begins.
/// </summary>
public event RestoringEventHandler Restoring;
/// <summary>
/// Fires when restoring has finished.
/// </summary>
public event RestoredEventHandler Restored;
#endregion
#region Methods
// ...
/// <summary>
/// Raises the storing event.
/// </summary>
private void OnStoring()
{
// Check if there are any event handlers.
if(this.Storing != null)
{
// Raise the storing event.
this.Storing(this, null);
}
}
/// <summary>
/// Raises the stored event.
/// </summary>
private void OnStored()
{
// Check if there are any event handlers.
if(this.Stored != null)
{
// Raise the stored event.
this.Stored(this, null);
}
}
/// <summary>
/// Raises the restoring event.
/// </summary>
private void OnRestoring()
{
// Check if there are any event handlers.
if(this.Restoring != null)
{
// Raise the restoring event.
this.Restoring(this, null);
}
}
/// <summary>
/// Raises the restored event.
/// </summary>
private void OnRestored()
{
// Check if there are any event handlers.
if(this.Restored != null)
{
// Raise the restored event.
this.Restored(this, null);
}
}
/// <summary>
/// Stores the current instance in the specified memento.
/// </summary>
/// <param name="memento">The memento.</param>
public void Store(IMemento memento)
{
// Raise the storing event.
this.OnStoring();
// TODO gr: Store the current instance.
// 2007-06-25
// Raise the stored event.
this.OnStored();
}
/// <summary>
/// Restores the current instance from the specified memento.
/// </summary>
/// <param name="memento">The memento.</param>
public void Restore(IMemento memento)
{
// Raise the restoring event.
this.OnRestoring();
// TODO gr: Restore the current instance.
// 2007-06-25
// Raise the restored event.
this.OnRestored();
}
#endregion
#region Constructors
#endregion
}
}
|
Damit ein außenstehendes Objekt auf ein Ereignis reagieren kann, muss es eine Methode als
ereignisbehandelnde Methode an dem Ereignis registrieren. Da Ereignisse intern nichts anderes
als Delegaten sind, entspricht die Vorgehensweise zum Registrieren und Deregistrieren der
zum Binden und Lösen von Methoden an Delegaten - der einzige Unterschied ist, dass für
Ereignisse nur die Varianten mit den Operatoren += und -= zulässig sind. Eine direkte
Zuweisung einer Methode oder das Zuweisen des Wertes
null sind nicht möglich.
Als Namensrichtlinie gilt, dass eine ereignisbehandelnde Methode dem Namen des
ereignisauslösenden Objekts, ergänzt um einen Unterstrich und den Namen des Ereignisses
entspricht, wobei jeweils Pascal Case angewandt wird. Eine Methode, die das Ereignis
Stored der Klasse ComplexNumber behandelt, hieße also ComplexNumber_Stored.