ein Projekt von goloroden.de

Ereignisse

Was sind Ereignisse?

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.

Auslösen von Ereignissen

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
    }
}

Reagieren auf Ereignisse

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.