C# Wo wird ein Event üblicherweise definiert?

Phoenixz

Lieutenant
Registriert
März 2004
Beiträge
595
Hallo,

die Frage klingt zugegebenermaßen deutlich einfacher als sie wahrscheinlich ist. Auch denke ich mir, dass es nicht DIE Antwort gibt. Aber zunächst zu meinem Problem (was ich anhand eines einfachen Beispiel erklären werde, das jedoch zugegebenermaßen eher abstrakter Natur ist):

Angenommen ich habe eine Klasse Car die eine Liste von Door-Objekten enthält. Es ist anzunehmen, das in dieser Klasse mehrere Threads arbeiten und z.B. eine Tür öffnen ;). Jetzt möchte ich einem Benutzer meiner Car-Klasse die Möglichkeit geben über Events mitzubekommen, wann eine Tür (Door-Objekt) geöffnet worden ist.

Dazu habe ich als Entwickler der Car-Klasse im Prinzip zwei (bis drei) Möglichkeiten:
1) Ich implementiere ein Opened-Event in der Door-Klasse.
2) Ich implementiere in meiner Car-Klasse ein DoorOpened-Event mit einer eigenen DoorOpenedEventArgs-Klasse, in der ich eine Referenz zum Door-Objekt ablege.
3) Ich implementiere sowohl 1) als auch 2).

Keine Frage, im Prinzip klappen alle drei Möglichkeiten ohne Probleme. Ich kann mir auch gut vorstellen, dass das ganze auch ein wenig vom Anwendungsszenario der Car-Klasse abhängig ist. Mich interessiert hierbei aber eher ob es eine Art „Empfehlung“ gibt. Ich habe mir selbst einige Gedanken dazu gemacht. Von der Semantik macht natürlich 1) am meisten Sinn. Aber das hat wiederum den Nachteil des der Benutzer für jedes Door-Objekt selbst einen EventHandler anmelden muss. Anders würde es mit 2) aussehen. Dort muss der Benutzer der Car-Klasse nichtmal zugriff auf die Door-Objekte haben, er kann einfach nur ein EventHandler für das DoorOpened-Event anmelden.

Aktuell arbeite ich an einer sehr großen Klassenbibliothek wo es quasi eine zentrale Klasse gibt. Und nun Frage ich mich ob ich da alle relevanten Events anbieten soll oder doch lieber in den jeweiligen Unterklassen.

Vielen Dank,
Daniel
 
Zuletzt bearbeitet:
Sorry, aber hast du dir meinen Text wirklich durchgelesen? Deinem Post nach würde ich eher nein vermuten.
 
Zuletzt bearbeitet:
Was du suchst ist glaube ich das Listener Konzept siehe z.b. hier.

Du hast einfach in jeder deiner Klassen einen Türstatus-geändert Listener, der z.b. ne Variable (doorOpen etc. ) entsprechend setzt.
 
Danke für die Antwort. Bei dem von dir genannten Konzept würde es allerdings eher um die technische Umsetzung von 2) gehen. Quasi "Wie bekomme ich mit, dass sich eine Tür geöffnet hat?". Die Technische Umsetzung ist allerdings nicht das Problem. Trotzdem, nochmals vielen Danke (an euch beide).

Wie auch schon Bagbag tendiere ich zu der zweiten (oder dritten Option). Insb. auch in der Hinsicht, dass bei meiner eigentlich Klassenbibliothek auch Objekte der Liste gelöscht werden können, müsste sich der Benutzer, insofern er den sauber Arbeiten will, auch noch um die Abmeldung der EventHandler kümmern.

Im Prinzip wäre die dritte Option wohl die sauberste. Dabei würde man quasi beim Car selbst EventHandler für alle Doors anmelden (die wiederum ein Opened-Event besitzen) und dann das entsprechende DoorOpendEvent im EventHandler werfen.
 
Ich würde auch sagen, dass sowohl die Türen ein onOpen()-Event feuern sollten, als auch das Auto. Würde für mich ein bisschen davon abhängen, wer von außen auf deine Klassen zugreift. Kann man nur Autos instantiieren oder auch einzelne Türen? Wenn beides geht dann auf jeden Fall Option 3, sonst tendiere ich zu 2.
 
Nein, ein Door Objekt (und auch alle anderen Teile) kann man nicht erzeugen (bzw. nur innerhalb der Klassenbibliothek selbst, also der Zugriffsmodifikation ist internal). Allerdings kann man durch alle Doors durchinterieren.
Ich habe mich jetzt nach reiflicher Überlegung für die dritte Möglichkeit (welche in meiner konkreten Klassenbibliothek leider auch die aufwendigste ist) entscheiden. Eigentlich wollte ich mich davor drücken, doch diese Option scheint wirklich der „way to go“ zu sein.

Ich habe mal ein kleines Beispiel „on the fly“ geschrieben, damit man versteht was ich meine.

Code:
    public class Door
    {
        ...

        public event EventHandler<OpenedEventArgs> Opened;
    }

Code:
    public class Car
    {
        private List<Door> doors = new List<Door>();

        public void addNewDoor()
        {
            Door door = new Door();
            door.Opened +=new EventHandler<OpenedEventArgs>(doorOpened);
            doors.Add(door);
        }

        void doorOpened(object sender, OpenedEventArgs e)
        {
            if (DoorOpened != null)
            {
                DoorOpenedEventArgs args = new DoorOpenedEventArgs();
                args.Door = (Door)sender;
                DoorOpened.Invoke(this, args);
            }
        }

        public event EventHandler<DoorOpenedEventArgs> DoorOpened;
    }

ggf. Syntaxfehler ignorieren, da ich das ohne IDE geschrieben habe (da ich gerade unterwegs bin).
 
Zuletzt bearbeitet:
Wenn man die Door nicht selbst instanziieren kann, sollte die Car-Klasse meiner Meinung ein entsprechendes Event anbieten. Dazu ein Event auf der Door-Klasse zu deklarieren und von der Car-Klasse abonnieren zu lassen ist dazu eine gute Idee.

Noch etwas zum Code: Ein Event sollte man nie direkt (erst recht nicht mit Invoke, wenn nicht nötig), sondern immer über eine eigene "protected virtual On[Event-Name](EventArgs)"-Methode auslösen. Darin kann man auch gleich die Null-Prüfung einbauen:
Code:
    public class Car
    {
        private List<Door> doors = new List<Door>();

        public void AddNewDoor()
        {
            Door door = new Door();
            door.Opened += HandleDoorOpened;
            doors.Add(door);
        }

        // Würde ich eigentlich auch "OnDoorOpened" nennen. Hab ich hier mal nicht gemacht um den Unterschied deutlich zu machen.
        private void HandleDoorOpened(object sender, OpenedEventArgs e)
        {
            DoorOpenedEventArgs args = new DoorOpenedEventArgs();
            args.Door = (Door)sender;

            OnDoorOpened(args);
        }

        public event EventHandler<DoorOpenedEventArgs> DoorOpened;

        protected virtual void OnDoorOpened(DoorOpenendEventArgs e)
        {
            // Den Handler-Delegate zwischenspeichern um Race-Conditions zu vermeiden.
            // In einer (nicht thread-safen) Library  nicht unbedingt nötig aber es schadet auch nicht.
            EventHandler<DoorOpenedEventArgs> handler = DoorOpened;
            if (handler != null)
            {
                handler(this, e);
            }
        }
    }

Ach und: Methoden-Namen schreibt man in C# groß. ;)
 
DANKE!

Das mit dem direkten aufrufen von Events stimmt natürlich absolut. Danke das du das hier nochmal geschrieben hast. Ich habe es allerdings nur aufgrund der fehlenden IDE (sitze gerade im Zug und habe auf meinen Arbeitslaptop kein VS installiert) einfach mal weggelassen ;). Aber das kann ja jeder behaupten ;).

Edit:
Was ich allerdings nicht wusste ist das mit .Invoke(...). Ich dachte immer EventName(...) wäre eine kurzschreibweise für EventName.Invoke(...). Gibts da einen Unterschied?

Edit2:
Ach und: Methoden-Namen schreibt man in C# groß.
Um ganz korrekt zu sein, public-Methodennamen schreibt man groß ^^. Immer dieser ständige wechsel zwischen Java und C# bringt einen ganz durcheinander ^^. Naja bald nie wieder Java... (aber das ist ein anderen Thema).

Nachtrag 01.03.2013 16:04
Mir ist bei dem von mir oben beschriebenen Konzept eine „interessante“ Problematik aufgefallen. Bleibt man bei dem Car Beispiel würde man bei einer RemoveDoor(Door door) den EventHandler auch wieder abmelden. Soweit so gut, durch das AddNewDoor() und RemoveDoor(Door door) würde man keine Speicherlecks verursachen. Allerdings gibt es auch folgendes Szenario:
Code:
Car car = new Car();
car.AddNewDoor();
car.AddNewDoor();
...
car.AddNewDoor();
car = null; // n agemeldete EventHandler die niemals abgemeldet worden sind.

Ich nehme an, dass das nicht unbedingt schön ist? Sicherlich könnte man jetzt vielleicht auf die Idee kommen zu argumentieren, dass der GC das dann „aufräumt“, doch dann könnte man ja gleich das Abmelden unterlassen, wenn der GC das Aufräumen würde. Also davon halte ich nicht viel ;).
Das einzige was mir da einfallen würde ist die IDisposable Schnittstelle zu implementieren und da alle Ressourcen (EventHandler) freizugeben (bzw. abzumelden). Führt der Benutzer den oben genannten Code aus, würde es immer noch zu einem Speicherleck führen, allerdings hat er auch die Möglichkeit das Objekt richtig über die Dispose Methode freizugeben.

Was meint ihr dazu?
 
Zuletzt bearbeitet: (Nachtrag)
korrekt und deshalb sind Events böse...
Aber wenn man nunmal darauf angewiesen ist, dann gehts nicht anders.
Ich bild mir ein, dass in C# aber der Using-Block ausreicht, oder täusch ich mich da?
 
Kommt drauf an was du genau meinst.
Ein using Block kannst du nur für Objekte benutzen, die die IDisposable Schnittstelle implementieren.
So ist

Code:
Car car = new Car();
...
car.Dispose();

und

Code:
using (Car car = new Car())
{
...
}

nahezu identisch. Auf die bzw. den einzigen Unterschied möchte ich hier nicht eingehen ;).

Allerdings muss, wie bereits gesagt das Objekt die Schnittstelle implementieren, also

Code:
public Car : IDisposable
{

public Dispose() //Oder wie die Schnittstelle genau heißt... nagelt mich jetzt bitte nicht auf die Syntax bzw. die Members fest ;)
{
...
}

}
 
Phoenixz schrieb:
Was ich allerdings nicht wusste ist das mit .Invoke(...). Ich dachte immer EventName(...) wäre eine kurzschreibweise für EventName.Invoke(...). Gibts da einen Unterschied?
Im IL-Code gibts tatsächlich keinen Unterschied. Allerdings ist deine Variante (zumindest für mich) ziemlich unüblich.

Phoenixz schrieb:
Um ganz korrekt zu sein, public-Methodennamen schreibt man groß ^^.
Ich meine schon alle Methoden. In Java schreibt man alle klein, in C# alle groß. Aber bevor das wieder in eine große Diskussion über Code-Styles ausartet, lassen wir das. Wirklich wichtig ist das eh nicht.

Phoenixz schrieb:
Mir ist bei dem von mir oben beschriebenen Konzept eine „interessante“ Problematik aufgefallen. Bleibt man bei dem Car Beispiel würde man bei einer RemoveDoor(Door door) den EventHandler auch wieder abmelden. Soweit so gut, durch das AddNewDoor() und RemoveDoor(Door door) würde man keine Speicherlecks verursachen. Allerdings gibt es auch folgendes Szenario:
Code:
Car car = new Car();
car.AddNewDoor();
car.AddNewDoor();
...
car.AddNewDoor();
car = null; // n agemeldete EventHandler die niemals abgemeldet worden sind.

Ich nehme an, dass das nicht unbedingt schön ist? Sicherlich könnte man jetzt vielleicht auf die Idee kommen zu argumentieren, dass der GC das dann „aufräumt“, doch dann könnte man ja gleich das Abmelden unterlassen, wenn der GC das Aufräumen würde.
Nein, der GC kann das auch gar nicht aufräumen, da die Doors durch die EventHandler noch eine Referenz auf das Car halten.

Phoenixz schrieb:
Das einzige was mir da einfallen würde ist die IDisposable Schnittstelle zu implementieren und da alle Ressourcen (EventHandler) freizugeben (bzw. abzumelden). Führt der Benutzer den oben genannten Code aus, würde es immer noch zu einem Speicherleck führen, allerdings hat er auch die Möglichkeit das Objekt richtig über die Dispose Methode freizugeben.
Nur wegen EventHandlern IDisposable zu implementieren halte ich für übertrieben. Zumal du ja nicht sicherstellen kannst, das Dispose auch aufgerufen wird.
Die Lösung ist das Weak Event Pattern. In der WPF ist wie im Artikel beschrieben schon ein solcher Mechanismus enthalten, im Netz gibts zu dem Thema auch einige Ansätze. Ziel ist es nur eine WeakReference auf die abonnierende Klasse zu halten.
 
Zurück
Oben