Standardereignismuster in .NETStandard .NET event patterns

VorherigePrevious

.NET-Ereignisse folgen in der Regel einigen bekannten Mustern..NET events generally follow a few known patterns. Standardisierung auf diese Muster bedeutet, dass Entwickler Kenntnisse über diese Standardmuster nutzen können, die auf ein beliebiges .NET Ereignisprogramm angewendet werden können.Standardizing on these patterns means that developers can leverage knowledge of those standard patterns, which can be applied to any .NET event program.

Schauen wir uns diese Standardmuster an, sodass Sie über alle Kenntnisse verfügen, die Sie benötigen, um Standardereignisquellen zu erstellen und Standardereignisse in Ihrem Code zu abonnieren und zu verarbeiten.Let's go through these standard patterns so you will have all the knowledge you need to create standard event sources, and subscribe and process standard events in your code.

EreignisdelegatsignaturenEvent Delegate Signatures

Die Standardsignatur für ein .NET-Ereignisdelegat lautet:The standard signature for a .NET event delegate is:

void OnEventRaised(object sender, EventArgs args);

Der Rückgabetyp ist void.The return type is void. Ereignisse basieren auf Delegaten und sind Multicastdelegaten.Events are based on delegates and are multicast delegates. Dies unterstützt mehrere Abonnenten für jede Ereignisquelle.That supports multiple subscribers for any event source. Der einzelne Rückgabewert einer Methode wird nicht auf mehrere Ereignisabonnenten skaliert.The single return value from a method doesn't scale to multiple event subscribers. Welchen Rückgabewert findet die Ereignisquelle nach dem Auslösen eines Ereignisses vor?Which return value does the event source see after raising an event? In diesem Artikel sehen Sie später, wie Sie Ereignisprotokolle zur Unterstützung der Ereignisabonnenten, die Informationen an die Ereignisquelle senden, erstellen.Later in this article you'll see how to create event protocols that support event subscribers that report information to the event source.

Die Argumentliste enthält zwei Argumente: Den Absender und die Ereignisargumente.The argument list contains two arguments: the sender, and the event arguments. Der Kompilierzeittyp von sender ist System.Object, obwohl Sie wahrscheinlich einen stärker abgeleiteten Typ kennen, die immer korrekt wäre.The compile time type of sender is System.Object, even though you likely know a more derived type that would always be correct. Verwenden Sie gemäß der Konvention object.By convention, use object.

Das zweite Argument ist in der Regel ein Typ, der von System.EventArgs abgeleitet ist.The second argument has typically been a type that is derived from System.EventArgs. (Im nächsten Abschnitt werden Sie sehen, dass diese Konvention nicht mehr erzwungen wird.) Wenn der Ereignistyp keine zusätzlichen Argumente benötigt, werden Sie dennoch beide Argumente bereitstellen.(You'll see in the next section that this convention is no longer enforced.) If your event type does not need any additional arguments, you will still provide both arguments. Es gibt einen speziellen Wert, EventArgs.Empty, den Sie verwenden sollten, um anzugeben, dass Ihr Ereignis keine weiteren Informationen enthält.There is a special value, EventArgs.Empty that you should use to denote that your event does not contain any additional information.

Erstellen Sie eine Klasse, die Dateien in einem Verzeichnis oder einem seiner Unterverzeichnisse, die einem Muster folgen, auflistet.Let's build a class that lists files in a directory, or any of its subdirectories that follow a pattern. Diese Komponente löst ein Ereignis für jede gefundene Datei aus, die mit dem Muster übereinstimmt.This component raises an event for each file found that matches the pattern.

Ein Ereignismodell bietet einige Vorteile beim Entwurf.Using an event model provides some design advantages. Sie können mehrere Ereignislistener erstellen, die verschiedene Aktionen ausführen, wenn eine gesuchte Datei gefunden wird.You can create multiple event listeners that perform different actions when a sought file is found. Das Kombinieren der verschiedenen Listener kann robustere Algorithmen erstellen.Combining the different listeners can create more robust algorithms.

Hier ist die erste Ereignisargumentdeklaration für die Suche nach einer gesuchten Datei:Here is the initial event argument declaration for finding a sought file:

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.Even though this type looks like a small, data-only type, you should follow the convention and make it a reference (class) type. Dies bedeutet, dass das Argumentobjekt als Verweis übergeben wird, und alle Updates der Daten von allen Abonnenten gesehen werden.That means the argument object will be passed by reference, and any updates to the data will be viewed by all subscribers. Die erste Version ist ein unveränderliches Objekt.The first version is an immutable object. Sie sollten die Eigenschaften im Ereignisargumenttyp auf unveränderlich einstellen.You should prefer to make the properties in your event argument type immutable. Auf diese Weise kann ein Abonnent die Werte nicht ändern, bevor ein anderer Abonnent sie sieht.That way, one subscriber cannot change the values before another subscriber sees them. (Es gibt Ausnahmen dafür, wie Sie unten sehen werden.)(There are exceptions to this, as you'll see below.)

Als Nächstes müssen wir die Ereignisdeklaration in der FileSearcher-Klasse erstellen.Next, we need to create the event declaration in the FileSearcher class. Die Nutzung des EventHandler<T>-Typ bedeutet, dass Sie nicht noch eine Typdefinition erstellen müssen.Leveraging the EventHandler<T> type means that you don't need to create yet another type definition. Sie verwenden einfach eine generische Spezialisierung.You simply use a generic specialization.

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.Let's fill out the FileSearcher class to search for files that match a pattern, and raise the correct event when a match is discovered.

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 von feldähnlichen EreignissenDefinining and Raising Field-Like Events

Die einfachste Möglichkeit, Ihrer Klasse ein Ereignis hinzuzufügen, ist das Deklarieren des Ereignisses als öffentliches Feld, wie im obigen Beispiel:The simplest way to add an event to your class is to declare that event as a public field, as in the above example:

public event EventHandler<FileFoundArgs> FileFound;

Dies scheint ein öffentliches Feld zu deklarieren, das als eine Vorgehensweise eines ungültigen Objekts erscheinen würde.This looks like it's declaring a public field, which would appear to be bad object oriented practice. Sie möchten den Datenzugriff über Eigenschaften oder Methoden schützen.You want to protect data access through properties, or methods. Während dies anscheinend kein empfehlenswertes Verfahren ist, erstellt der vom Compiler generierte Code Wrapper, damit auf die Ereignisobjekte nur auf sichere Weise zugegriffen werden kann.While this make look like a bad practice, the code generated by the compiler does create wrappers so that the event objects can only be accessed in safe ways. Die einzig verfügbaren Vorgänge für ein feldähnliches Ereignis sind das Hinzufügen von Handlern:The only operations available on a field-like event are add handler:

EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
    Console.WriteLine(eventArgs.FoundFile);
lister.FileFound += onFileFound;

und das Entfernen von Handlern:and remove handler:

lister.FileFound -= onFileFound;

Beachten Sie, dass eine lokale Variable für den Handler vorhanden ist.Note that there's a local variable for the handler. Wenn Sie den Text des Lambda-Ausdrucks verwenden, würde das Entfernen nicht ordnungsgemäß funktionieren.If you used the body of the lambda, the remove would not work correctly. Es wäre eine andere Instanz des Delegaten, und es würde stillschweigend nichts geschehen.It would be a different instance of the delegate, and silently do nothing.

Code außerhalb der Klasse kann das Ereignis nicht auslösen, noch kann er andere Vorgänge ausführen.Code outside the class cannot raise the event, nor can it perform any other operations.

Rückgabe von Werten aus EreignisabonnentenReturning Values from Event Subscribers

Ihre einfache Version funktioniert einwandfrei.Your simple version is working fine. Fügen wir eine weitere Funktion hinzu: Abbruch.Let's add another feature: Cancellation.

Beim Auslösen des gefunden Ereignisses sollten Listener die weitere Verarbeitung beenden können, wenn diese Datei die letzte gesuchte Datei ist.When you raise the found event, listeners should be able to stop further processing, if this file is that last one sought.

Die Ereignishandler geben keinen Wert zurück. Daher müssen Sie dies auf andere Weise kommunizieren.The event handlers do not return a value, so you need to communicate that in another way. Das Standardereignismuster verwendet das EventArgs-Objekt, um Felder einzuschließen, die Ereignisabonnenten zum Kommunizieren von „Abbrechen“ verwenden.The standard event pattern uses the EventArgs object to include fields that event subscribers can use to communicate cancel.

Es gibt zwei unterschiedliche Muster, die verwendet werden können, auf der Grundlage der Semantik des Vertrags „Abbrechen“.There are two different patterns that could be used, based on the semantics of the Cancel contract. In beiden Fällen werden Sie ein boolesches Feld zum EventArguments für das gefundene Dateiereignis hinzufügen.In both cases, you'll add a boolean field to the EventArguments for the found file event.

Ein Muster ermöglicht jedem Abonnenten, den Vorgang abzubrechen.One pattern would allow any one subscriber to cancel the operation. Für dieses Muster wird ein neues Feld mit false initialisiert.For this pattern, the new field is initialized to false. Jeder Abonnent kann es in true ändern.Any subscriber can change it to true. Nachdem alle Abonnenten das ausgelöste Ereignis gesehen haben, untersucht die Komponente FileSearcher den booleschen Wert und ergreift Maßnahmen.After all subscribers have seen the event raised, the FileSearcher component examines the boolean value and takes action.

Das zweite Muster würde nur den Vorgang abbrechen, wenn alle Abonnenten den Vorgang abgebrochen haben möchten.The second pattern would only cancel the operation if all subscribers wanted the operation cancelled. 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.In this pattern, the new field is initialized to indicate the operation should cancel, and any subscriber could change it to indicate the operation should continue. Nachdem alle Abonnenten das ausgelöste Ereignis gesehen haben, untersucht die Komponente FileSearcher den booleschen Wert und ergreift Maßnahmen.After all subscribers have seen the event raised, the FileSearcher component examines the boolean and takes action. Es gibt einen zusätzlichen Schritt in diesem Muster: Die Komponente muss wissen, ob jeder Abonnent das Ereignis gesehen hat.There is one extra step in this pattern: the component needs to know if any subscribers have seen the event. Wenn keine Abonnenten vorhanden sind, würde das Feld fälschlicherweise einen Abbruch angeben.If there are no subscribers, the field would indicate a cancel incorrectly.

Implementieren wir die erste Version für dieses Beispiel.Let's implement the first version for this sample. Sie müssen ein boolesches Feld zum FileFoundEventArgs-Typ hinzufügen:You need to add a boolean field to the FileFoundEventArgs type:

public class FileFoundArgs : EventArgs
{
    public string FoundFile { get; }
    public bool CancelRequested { get; set; }

    public FileFoundArgs(string fileName)
    {
        FoundFile = fileName;
    }
}

Dieses neue Feld sollte mit FALSE initialisiert werden, damit Sie nicht ohne Grund abbrechen.This new Field should be initialized to false, so you don't cancel for no reason. Das ist der Standardwert für ein boolesches Feld, damit dies automatisch geschieht.That is the default value for a boolean field, so that happens automatically. Die einzige andere Änderung der Komponente ist das Überprüfen des Flag nach dem Auslösen des Ereignisses, um festzustellen, ob einer der Abonnenten einen Abbruch angefordert hat:The only other change to the component is to check the flag after raising the event to see if any of the subscribers have requested a cancellation:

public void List(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.One advantage of this pattern is that it isn't a breaking change. Keiner der Abonnenten hat vorher einen Abbruch angefordert und macht es noch immer nicht.None of the subscribers requested a cancel before, and they still are not. Kein Teil des Abonnentencodes benötigt eine Aktualisierung, sofern sie das neue Abbrechen-Protokoll nicht unterstützen möchten.None of the subscriber code needs updating unless they want to support the new cancel protocol. Es ist sehr lose gekoppelt.It's very loosely coupled.

Aktualisieren wir den Abonnenten, damit ein Abbruch angefordert wird, sobald die erste ausführbare Datei gefunden wird:Let's update the subscriber so that it requests a cancellation once it finds the first executable:

EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
    Console.WriteLine(eventArgs.FoundFile);
    eventArgs.CancelRequested = true;
};

Hinzufügen einer anderen EreignisdeklarationAdding Another Event Declaration

Fügen wir eine weitere Funktion hinzu, und zeigen andere Sprachausdrücke für Ereignisse.Let's add one more feature, and demonstrate other language idioms for events. Fügen wir eine Überladung der Search()-Methode, die alle Unterverzeichnisse auf der Suche nach Dateien durchsucht.Let's add an overload of the Search() method that traverses all subdirectories in search of files.

In einem Verzeichnis mit vielen Unterverzeichnisse könnte dies ein längerer Vorgang werden.This could get to be a lengthy operation in a directory with many sub-directories. Fügen wir ein Ereignis hinzu, das zu Beginn jeder neuen Verzeichnissuche ausgelöst wird.Let's add an event that gets raised when each new directory search begins. Dies ermöglicht es Abonnenten, den Fortschritt zu verfolgen und den Benutzer während des Fortschritts zu aktualisieren.This enables subscribers to track progress, and update the user as to progress. Die Beispiele, die Sie bisher erstellt haben, sind öffentlich.All the samples you've created so far are public. Dieses erstellen wir als internes Ereignis.Let's make this one an internal event. Das bedeutet, dass Sie auch die Typen für die Argumente intern erstellen können.That means you can also make the types used for the arguments internal as well.

Sie beginnen mit dem Erstellen der neuen abgeleiteten EventArgs-Klasse für die Berichte des neuen Verzeichnisses und Status.You'll start by creating the new EventArgs derived class for reporting the new directory and progress.

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.Again, you can follow the recommendations to make an immutable reference type for the event arguments.

Definieren Sie als Nächstes das Ereignis.Next, define the event. Dieses Mal werden Sie eine andere Syntax verwenden.This time, you'll use a different syntax. Zusätzlich zur Verwendung der Feldsyntax, können Sie die Eigenschaft explizit erstellen, mit dem Hinzufügen- und Entfernen-Handler.In addition to using the field syntax, you can explicitly create the property, with add and remove handlers. In diesem Beispiel werden Sie für diese Handler in diesem Projekt keinen zusätzlichen Code benötigen, aber dies zeigt, wie Sie sie erstellen würden.In this sample, you won't need extra code in those handlers in this project, but this shows how you would create them.

internal event EventHandler<SearchDirectoryArgs> DirectoryChanged
{
    add { directoryChanged += value; }
    remove { directoryChanged -= value; }
}
private EventHandler<SearchDirectoryArgs> directoryChanged;

In vielerlei Hinsicht spiegelt der Code, den Sie hier schreiben, den vom Compiler generierten Code für die Feldereignisdefinitionen wider, die Sie zuvor gesehen haben.In may ways, the code you write here mirrors the code the compiler generates for the field event definitions you've seen earlier. Sie erstellen das Ereignis mithilfe der Syntax ähnlich der für Eigenschaften.You create the event using syntax very similar to that used for properties. Beachten Sie, dass die Handler unterschiedliche Namen haben: add und remove.Notice that the handlers have different names: add and remove. Diese werden aufgerufen, um das Ereignis zu abonnieren oder sich vom Ereignis abzumelden.These are called to subscribe to the event, or unsubscribe from the event. Beachten Sie, dass Sie auch ein privates Unterstützungsfeld zum Speichern der Ereignisvariable deklarieren müssen.Notice that you also must declare a private backing field to store the event variable. Es wird mit NULL initialisiert.It is initialized to null.

Als Nächstes fügen wir die Überladung der Search()-Methode hinzu, die Unterverzeichnisse durchsucht und beide Ereignisse auslöst.Next, let's add the overload of the Search() method that traverses subdirectories and raises both events. Die einfachste Möglichkeit besteht darin, ein Standardargument zu verwenden, um anzugeben, dass alle Verzeichnisse durchsucht werden sollen:The easiest way to accomplish this is to use a default argument to specify that you want to search all directories:

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 SearchDirectoryArgs(dir, totalDirs, completedDirs++));
            // Recursively search this child directory:
            SearchDirectory(dir, searchPattern);
        }
        // Include the Current Directory:
        directoryChanged?.Invoke(this,
            new SearchDirectoryArgs(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;
    }
}

An diesem Punkt können Sie die Anwendung durch das Aufrufen der Überladung für die Suche aller Unterverzeichnisse ausführen.At this point, you can run the application calling the overload for searching all sub-directories. Es sind keine Abonnenten auf dem neuen ChangeDirectory-Ereignis vorhanden, aber mit den ?.Invoke()-Ausdruck wird sichergestellt, dass es ordnungsgemäß funktioniert.There are no subscribers on the new ChangeDirectory event, but using the ?.Invoke() idiom ensures that this works correctly.

Fügen wir einen Handler hinzu, um eine Zeile zu schreiben, die den Status im Konsolenfenster anzeigt.Let's add a handler to write a line that shows the progress in the console window.

lister.DirectoryChanged += (sender, eventArgs) =>
{
    Console.Write($"Entering '{eventArgs.CurrentSearchDirectory}'.");
    Console.WriteLine($" {eventArgs.CompletedDirs} of {eventArgs.TotalDirs} completed...");
};

Sie haben Muster gesehen, die im .NET-Ökosystem eingehalten werden.You've seen patterns that are followed throughout the .NET ecosystem. Indem Sie diese Muster und Konventionen erlernen, werden Sie schnell idiomatische C# und .NET schreiben können.By learning these patterns and conventions, you'll be writing idiomatic C# and .NET quickly.

Als Nächstes sehen Sie einige Änderungen in diesen Mustern in der neuesten Version von .NET.Next, you'll see some changes in these patterns in the most recent release of .NET.

NächsteNext