Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
.NET-Ereignisse folgen in der Regel einigen bekannten Mustern. Die Standardisierung dieser Muster bedeutet, dass Entwickler Kenntnisse dieser Standardmuster anwenden können, die auf jedes .NET-Ereignisprogramm angewendet werden können.
Lassen Sie uns diese Standardmuster durchgehen, damit Sie alle erforderlichen Kenntnisse haben, um Standardereignisquellen zu erstellen und Standardereignisse in Ihrem Code zu abonnieren und zu verarbeiten.
Ereignisdelegatsignaturen
Die Standardsignatur für ein .NET-Ereignisdelegat lautet:
void EventRaised(object sender, EventArgs args);
Diese Standardsignatur bietet Einblicke, wann Ereignisse verwendet werden.
- Der Rückgabetyp ist ungültig. Ereignisse können null bis viele Zuhörer haben. Das Auslösen eines Ereignisses benachrichtigt alle Listener. Im Allgemeinen stellen Listener keine Werte als Reaktion auf Ereignisse bereit.
-
Ereignisse geben den Absender an: Die Ereignissignatur enthält das Objekt, das das Ereignis ausgelöst hat. Dies bietet jedem Listener einen Mechanismus für die Kommunikation mit dem Absender. Der Kompilierzeittyp von
sender
lautetSystem.Object
, obwohl Sie wahrscheinlich einen stärker abgeleiteten Typ kennen, der immer richtig wäre. Verwenden Sie gemäß der Konventionobject
. -
Ereignisse packen weitere Informationen in eine einzelne Struktur: Der
args
-Parameter ist ein Typ, der von System.EventArgs abgeleitet ist und alle erforderlichen Informationen enthält. (Im nächsten Abschnitt wird angezeigt, dass diese Konvention nicht mehr erzwungen wird.) Wenn Ihr Ereignistyp keine weiteren Argumente benötigt, müssen Sie dennoch beide Argumente angeben. Es gibt einen speziellen Wert, den Sie verwenden sollten, EventArgs.Empty um anzugeben, dass Ihr Ereignis keine zusätzlichen Informationen enthält.
Erstellen Sie eine Klasse, die Dateien in einem Verzeichnis oder einem seiner Unterverzeichnisse, die einem Muster folgen, auflistet. Diese Komponente löst ein Ereignis für jede gefundene Datei aus, die mit dem Muster übereinstimmt.
Ein Ereignismodell bietet einige Vorteile beim Entwurf. Sie können mehrere Ereignislistener erstellen, die verschiedene Aktionen ausführen, wenn eine gesuchte Datei gefunden wird. Das Kombinieren der verschiedenen Listener kann robustere Algorithmen erstellen.
Dies ist die erste Ereignisargumentdeklaration für die Suche nach einer gesuchten Datei:
public class FileFoundArgs : EventArgs
{
public string FoundFile { get; }
public FileFoundArgs(string fileName) => FoundFile = fileName;
}
Obwohl dieser Typ wie ein kleiner, Nur-Daten-Typ aussieht, sollten Sie die Konvention einhalten und ihm einen Verweis (class
)-Typ geben. Das bedeutet, dass das Argumentobjekt per Verweis übergeben wird und alle Aktualisierungen der Daten von allen Abonnenten angezeigt werden. Die erste Version ist ein unveränderliches Objekt. Sie sollten die Eigenschaften im Ereignisargumenttyp auf unveränderlich einstellen. Auf diese Weise kann ein Abonnent die Werte nicht ändern, bevor ein anderer Abonnent sie sieht. (Es gibt Ausnahmen von dieser Praxis, wie Sie später sehen.)
Als Nächstes müssen wir die Ereignisdeklaration in der FileSearcher-Klasse erstellen. Die Verwendung des System.EventHandler<TEventArgs> Typs bedeutet, dass Sie keine weitere Typdefinition erstellen müssen. Sie verwenden einfach eine generische Spezialisierung.
Füllen wir die FileSearcher-Klasse aus, um nach Dateien mit dem Muster zu suchen und das richtige Ereignis auszulösen, wenn eine Übereinstimmung gefunden wird.
public class FileSearcher
{
public event EventHandler<FileFoundArgs>? FileFound;
public void Search(string directory, string searchPattern)
{
foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
{
FileFound?.Invoke(this, new FileFoundArgs(file));
}
}
}
Definieren und Auslösen feldähnlicher Ereignisse
Die einfachste Möglichkeit, Ihrer Klasse ein Ereignis hinzuzufügen, ist das Deklarieren des Ereignisses als öffentliches Feld, wie im vorherigen Beispiel:
public event EventHandler<FileFoundArgs>? FileFound;
Dies scheint ein öffentliches Feld zu deklarieren, was als schlechte objektorientierte Vorgehensweise erscheinen würde. Sie möchten den Datenzugriff über Eigenschaften oder Methoden schützen. Obwohl dieser Code möglicherweise wie eine schlechte Methode aussieht, erstellt der vom Compiler generierte Code Wrapper, sodass nur auf sichere Weise auf die Ereignisobjekte zugegriffen werden kann. Die einzigen Vorgänge, die für ein feldähnliches Ereignis verfügbar sind, sind Add- und Remove-Handler.
var fileLister = new FileSearcher();
int filesFound = 0;
EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
Console.WriteLine(eventArgs.FoundFile);
filesFound++;
};
fileLister.FileFound += onFileFound;
fileLister.FileFound -= onFileFound;
Es gibt eine lokale Variable für den Handler. Wenn Sie den Text des Lambda-Ausdrucks verwenden, würde der Handler remove
nicht ordnungsgemäß funktionieren. Es wäre eine andere Instanz des Delegaten, und es würde stillschweigend nichts geschehen.
Code außerhalb der Klasse kann das Ereignis weder auslösen noch andere Vorgänge ausführen.
Beginnend mit C# 14 können Ereignisse als Teilmitglieder deklariert werden. Eine Partielle Ereignisdeklaration muss eine definierende Deklaration und eine implementierende Deklaration enthalten. Die definierende Deklaration muss die feldähnliche Ereignissyntax verwenden. Die Implementierungsdeklaration muss die add
und remove
Handler deklarieren.
Zurückgeben von Werten von Ereignisabonnent*innen
Ihre einfache Version funktioniert einwandfrei. Fügen wir eine weitere Funktion hinzu: Abbruch.
Wenn Sie das Found-Ereignis auslösen, sollten Listener in der Lage sein, die weitere Verarbeitung zu beenden, wenn diese Datei der letzte gesuchte ist.
Die Ereignishandler geben keinen Wert zurück, daher müssen Sie dies auf eine andere Weise kommunizieren. Das Standardereignismuster verwendet das EventArgs
-Objekt, um Felder einzuschließen, die Ereignisabonnent*innen zum Kommunizieren des Abbruchs verwenden können.
Basierend auf der Grundlage der Semantik des Vertrags „Abbrechen“ können zwei unterschiedliche Muster verwendet werden. In beiden Fällen fügen Sie dem EventArguments für das gefundene Dateiereignis ein boolesches Feld hinzu.
Ein Muster ermöglicht jedem Abonnenten, den Vorgang abzubrechen. Für dieses Muster wird ein neues Feld mit false
initialisiert. Jeder Abonnent kann es in true
ändern. Nach dem Auslösen des Ereignisses für alle Abonnenten prüft die FileSearcher-Komponente den booleschen Wert und führt eine Aktion aus.
Das zweite Muster würde nur dann den Vorgang abbrechen, wenn alle Abonnent*innen möchten, dass er abgebrochen wird. In diesem Muster wird das neue Feld initialisiert, um anzugeben, dass der Vorgang abgebrochen werden soll, und jeder Abonnent kann dies ändern, um anzugeben, dass der Vorgang fortgesetzt werden soll. Nachdem alle Abonnenten das ausgelöste Ereignis verarbeitet haben, prüft die FileSearcher-Komponente den Booleschen Wert und ergreift Maßnahmen. In diesem Muster gibt es einen zusätzlichen Schritt: Die Komponente muss wissen, ob abonnenten auf das Ereignis geantwortet haben. Wenn keine Abonnenten vorhanden sind, würde das Feld fälschlicherweise einen Abbruch angeben.
Implementieren wir die erste Version für dieses Beispiel. Sie müssen ein boolesches Feld mit dem Namen CancelRequested
dem FileFoundArgs
-Typ hinzufügen:
public class FileFoundArgs : EventArgs
{
public string FoundFile { get; }
public bool CancelRequested { get; set; }
public FileFoundArgs(string fileName) => FoundFile = fileName;
}
Dieses neue Feld wird automatisch auf false
initialisiert, sodass Sie nicht versehentlich abbrechen. Die einzige andere Änderung an der Komponente besteht darin, die Kennzeichnung nach dem Auslösen des Ereignisses zu überprüfen, um festzustellen, ob eine der Abonnenten eine Stornierung angefordert hat:
private void SearchDirectory(string directory, string searchPattern)
{
foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
{
var args = new FileFoundArgs(file);
FileFound?.Invoke(this, args);
if (args.CancelRequested)
break;
}
}
Ein Vorteil dieses Musters ist, dass es keine unterbrechende Änderung ist. Keiner der Abonnenten hat zuvor eine Kündigung beantragt, und das tun sie immer noch nicht. Kein Abonnentcode erfordert Updates, es sei denn, sie möchten das neue Cancel-Protokoll unterstützen.
Aktualisieren wir den Abonnenten, damit ein Abbruch angefordert wird, sobald die erste ausführbare Datei gefunden wird:
EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
Console.WriteLine(eventArgs.FoundFile);
eventArgs.CancelRequested = true;
};
Hinzufügen einer anderen Ereignisdeklaration
Fügen wir eine weitere Funktion hinzu, und zeigen andere Sprachausdrücke für Ereignisse. Fügen wir eine Überladung der Search
-Methode, die alle Unterverzeichnisse auf der Suche nach Dateien durchsucht.
Diese Methode kann zu einem langwierigen Vorgang in einem Verzeichnis mit vielen Unterverzeichnissen führen. Fügen wir ein Ereignis hinzu, das zu Beginn jeder neuen Verzeichnissuche ausgelöst wird. Dieses Ereignis ermöglicht Es Abonnenten, den Fortschritt nachzuverfolgen und den Benutzer entsprechend zu aktualisieren. Alle Bisher erstellten Beispiele sind öffentlich. Lassen Sie uns dieses Ereignis zu einem internen Ereignis machen. Das bedeutet, dass Sie auch die Argumenttypen intern machen können.
Zunächst erstellen Sie die neue abgeleitete EventArgs-Klasse, um das neue Verzeichnis und den Fortschritt zu melden.
internal class SearchDirectoryArgs : EventArgs
{
internal string CurrentSearchDirectory { get; }
internal int TotalDirs { get; }
internal int CompletedDirs { get; }
internal SearchDirectoryArgs(string dir, int totalDirs, int completedDirs)
{
CurrentSearchDirectory = dir;
TotalDirs = totalDirs;
CompletedDirs = completedDirs;
}
}
In diesem Fall können Sie erneut der Empfehlung für das Erstellen eines nicht änderbaren Verweistyps für die Ereignisargumente folgen.
Definieren Sie als Nächstes das Ereignis. In diesem Fall verwenden Sie eine andere Syntax. Zusätzlich zur Verwendung der Feldsyntax können Sie die Ereigniseigenschaft explizit mit Add- und Remove-Handlern erstellen. In diesem Beispiel benötigen Sie keinen zusätzlichen Code in diesen Handlern, aber dies zeigt, wie Sie sie erstellen würden.
internal event EventHandler<SearchDirectoryArgs> DirectoryChanged
{
add { _directoryChanged += value; }
remove { _directoryChanged -= value; }
}
private EventHandler<SearchDirectoryArgs>? _directoryChanged;
In vielem spiegelt der Code, den Sie hier schreiben, den Code wider, den der Compiler für die zuvor gesehenen Feldereignisdefinitionen generiert. Sie erstellen das Ereignis mithilfe einer Syntax ähnlich wie Eigenschaften. Beachten Sie, dass die Handler unterschiedliche Namen haben: add
und remove
. Diese Accessoren werden aufgerufen, um das Ereignis zu abonnieren oder sich vom Ereignis abzumelden. Beachten Sie, dass Sie auch ein privates Unterstützungsfeld zum Speichern der Ereignisvariable deklarieren müssen. Diese Variable wird auf NULL initialisiert.
Als Nächstes fügen wir die Überladung der Search
-Methode hinzu, die Unterverzeichnisse durchsucht und beide Ereignisse auslöst. Die einfachste Möglichkeit besteht darin, ein Standardargument zu verwenden, um anzugeben, dass Sie alle Verzeichnisse durchsuchen möchten:
public void Search(string directory, string searchPattern, bool searchSubDirs = false)
{
if (searchSubDirs)
{
var allDirectories = Directory.GetDirectories(directory, "*.*", SearchOption.AllDirectories);
var completedDirs = 0;
var totalDirs = allDirectories.Length + 1;
foreach (var dir in allDirectories)
{
_directoryChanged?.Invoke(this, new (dir, totalDirs, completedDirs++));
// Search 'dir' and its subdirectories for files that match the search pattern:
SearchDirectory(dir, searchPattern);
}
// Include the Current Directory:
_directoryChanged?.Invoke(this, new (directory, totalDirs, completedDirs++));
SearchDirectory(directory, searchPattern);
}
else
{
SearchDirectory(directory, searchPattern);
}
}
private void SearchDirectory(string directory, string searchPattern)
{
foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
{
var args = new FileFoundArgs(file);
FileFound?.Invoke(this, args);
if (args.CancelRequested)
break;
}
}
Zu diesem Zeitpunkt können Sie die Anwendung ausführen, die eine Überladung aufruft, um alle Unterverzeichnisse zu durchsuchen. Es gibt keine Abonnenten für das neue DirectoryChanged
Ereignis, aber die Verwendung des ?.Invoke()
Idioms stellt sicher, dass es ordnungsgemäß funktioniert.
Fügen wir einen Handler hinzu, um eine Zeile zu schreiben, die den Status im Konsolenfenster anzeigt.
fileLister.DirectoryChanged += (sender, eventArgs) =>
{
Console.Write($"Entering '{eventArgs.CurrentSearchDirectory}'.");
Console.WriteLine($" {eventArgs.CompletedDirs} of {eventArgs.TotalDirs} completed...");
};
Sie haben Muster gesehen, die im gesamten .NET-Ökosystem befolgt werden. Indem Sie diese Muster und Konventionen erlernen, schreiben Sie idiomatische C# und .NET schnell.
Siehe auch
Als Nächstes werden einige Änderungen in diesen Mustern in der neuesten Version von .NET angezeigt.