Dela via


Standardmönster för .NET-händelser

Föregående

.NET-händelser följer vanligtvis några kända mönster. Standardisering av dessa mönster innebär att utvecklare kan använda kunskap om dessa standardmönster, som kan tillämpas på alla .NET-händelseprogram.

Nu ska vi gå igenom dessa standardmönster så att du har all kunskap du behöver för att skapa standardhändelsekällor och prenumerera på och bearbeta standardhändelser i koden.

Signaturer för händelsedelegat

Standardsignaturen för en .NET-händelsedelegat är:

void EventRaised(object sender, EventArgs args);

Den här standardsignaturen ger insikt i när händelser används:

  • Returtypen är ogiltig. Händelser kan ha från noll till många lyssnare. Genom att utlösa en händelse meddelas alla lyssnare. I allmänhet anger lyssnare inte värden som svar på händelser.
  • händelser anger avsändaren: Händelsesignaturen innehåller objektet som skapade händelsen. Det ger alla lyssnare en mekanism för att kommunicera med avsändaren. Kompileringstidstypen sender är System.Object, även om du förmodligen vet en mer härledd typ som alltid skulle vara korrekt. Använd enligt objectkonvention .
  • Händelsepaketet inkluderar mer information i en enda struktur: Parametern args är en typ som härleds från System.EventArgs som innehåller ytterligare nödvändig information. (Du ser i nästa avsnitt att den här konventionen inte längre tillämpas.) Om händelsetypen inte behöver fler argument måste du fortfarande ange båda argumenten. Det finns ett särskilt värde, EventArgs.Empty som du bör använda för att ange att händelsen inte innehåller någon ytterligare information.

Nu ska vi skapa en klass som visar filer i en katalog eller någon av dess underkataloger som följer ett mönster. Den här komponenten genererar en händelse för varje fil som hittas som matchar mönstret.

Att använda en händelsemodell ger vissa designfördelar. Du kan skapa flera händelselyssnare som utför olika åtgärder när en sök fil hittas. Genom att kombinera de olika lyssnarna kan du skapa mer robusta algoritmer.

Här är den första händelseargumentdeklarationen för att hitta en sökfil:

public class FileFoundArgs : EventArgs
{
    public string FoundFile { get; }

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

Även om den här typen ser ut som en liten datatyp bör du följa konventionen och göra den till en referenstyp (class). Det innebär att argumentobjektet skickas som referens, och alla uppdateringar av data visas av alla prenumeranter. Den första versionen är ett oföränderligt objekt. Du bör föredra att göra egenskaperna i händelseargumenttypen oföränderliga. På så sätt kan en prenumerant inte ändra värdena innan en annan prenumerant ser dem. (Det finns undantag till den här metoden, som du ser senare.)

Sedan måste vi skapa händelsedeklarationen i klassen FileSearcher. Om du använder System.EventHandler<TEventArgs> typ behöver du inte skapa ännu en typdefinition. Du använder bara en allmän specialisering.

Vi fyller i klassen FileSearcher för att söka efter filer som matchar ett mönster och skapa rätt händelse när en matchning identifieras.

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

Definiera och utlösa fältliknande händelser

Det enklaste sättet att lägga till en händelse i klassen är att deklarera händelsen som ett offentligt fält, som i föregående exempel:

public event EventHandler<FileFoundArgs>? FileFound;

Det verkar som om det deklarerar ett offentligt fält, vilket verkar vara en felaktig objektorienterad metod. Du vill skydda dataåtkomst via egenskaper eller metoder. Även om den här koden kan se ut som en dålig metod skapar koden som genereras av kompilatorn omsluter så att händelseobjekten bara kan nås på ett säkert sätt. De enda åtgärder som är tillgängliga för en fältliknande händelse är att lägga till och ta bort hanteraren:

var fileLister = new FileSearcher();
int filesFound = 0;

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

fileLister.FileFound += onFileFound;
fileLister.FileFound -= onFileFound;

Det finns en lokal variabel för hanteraren. Om du använde lambdakroppen skulle remove-hanteraren inte fungera korrekt. Det skulle vara en annan instans av ombudet och gör ingenting i tysthet.

Kod utanför klassen kan inte generera händelsen och kan inte heller utföra andra åtgärder.

Från och med C# 14 kan händelser deklareras som partiella medlemmar. En partiell händelsedeklaration måste innehålla en definierande deklaration och en genomförandedeklaration. Den definierande deklarationen måste använda fältliknande händelsesyntax. Implementeringsdeklarationen måste deklarera add- och remove-hanterarna.

Returnera värden från händelseprenumeranter

Din enkla version fungerar bra. Nu ska vi lägga till en annan funktion: Annullering.

När du genererar händelsen Hittade bör lyssnare kunna stoppa ytterligare bearbetning, om den här filen är den sista som söks.

Händelsehanterarna returnerar inte något värde, så du måste kommunicera det på ett annat sätt. Standardhändelsemönstret använder EventArgs objektet för att inkludera fält som händelseprenumeranter kan använda för att kommunicera avbryt.

Två olika mönster kan användas, baserat på semantiken i Avbryt-kontraktet. I båda fallen lägger du till ett booleskt fält i EventArguments för den hittade filhändelsen.

Ett mönster skulle göra det möjligt för alla prenumeranter att avbryta åtgärden. För det här mönstret initieras det nya fältet till false. Alla prenumeranter kan ändra den till true. När händelsen har höjts för alla prenumeranter undersöker FileSearcher-komponenten det booleska värdet och vidtar åtgärder.

Det andra mönstret skulle bara avbryta åtgärden om alla prenumeranter ville att åtgärden skulle avbrytas. I det här mönstret initieras det nya fältet för att indikera att åtgärden ska avbrytas, och alla prenumeranter kan ändra det för att indikera att åtgärden ska fortsätta. När alla prenumeranter har bearbetat den utlösta händelsen undersöker FileSearcher-komponenten boolean-värdet och vidtar åtgärder. Det finns ett extra steg i det här mönstret: komponenten måste veta om några prenumeranter svarade på händelsen. Om det inte finns några prenumeranter skulle fältet indikera att en avbokning är felaktig.

Nu ska vi implementera den första versionen för det här exemplet. Du måste lägga till ett booleskt fält med namnet CancelRequested till FileFoundArgs typen:

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

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

Det här nya fältet initieras automatiskt till false så att du inte avbryter av misstag. Den enda andra ändringen av komponenten är att kontrollera flaggan efter att händelsen har höjts för att se om någon av prenumeranterna begärde en annullering:

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

En fördel med det här mönstret är att det inte är en störande ändring. Ingen av prenumeranterna begärde annullering tidigare, och det är de fortfarande inte. Ingen av prenumerantkoden kräver uppdateringar om de inte vill ha stöd för det nya avbryt-protokollet.

Nu ska vi uppdatera prenumeranten så att den begär en annullering när den hittar den första körbara filen:

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

Lägga till ytterligare en händelsedeklaration

Låt oss lägga till ytterligare en funktion och demonstrera andra språkuttryck för händelser. Låt oss lägga till en överlagring av metoden Search som går igenom alla underkataloger för att hitta filer.

Den här metoden kan bli en lång åtgärd i en katalog med många underkataloger. Nu ska vi lägga till en händelse som aktiveras när varje ny katalogsökning börjar. Den här händelsen gör det möjligt för prenumeranter att spåra förloppet och uppdatera användaren om förloppet. Alla exempel som du har skapat hittills är offentliga. Nu ska vi göra den här händelsen till en intern händelse. Det innebär att du också kan göra argumenttyperna interna.

Du börjar med att skapa den nya EventArgs-härledda klassen för att rapportera den nya katalogen och förloppet.

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

Återigen kan du följa rekommendationerna för att göra en oföränderlig referenstyp för händelseargumenten.

Definiera sedan händelsen. Den här gången använder du en annan syntax. Förutom att använda fältsyntaxen kan du uttryckligen skapa händelseegenskapen med lägg till och ta bort hanterare. I det här exemplet behöver du inte extra kod i dessa hanterare, men det visar hur du skulle skapa dem.

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

På många sätt speglar koden du skriver här koden som kompilatorn genererar för de fälthändelsedefinitioner som du såg tidigare. Du skapar händelsen med syntax som liknar egenskaper. Observera att hanterarna har olika namn: add och remove. Dessa accessorer anropas för att prenumerera på händelsen eller avbryta prenumerationen på händelsen. Observera att du också måste deklarera ett privat bakgrundsfält för att lagra händelsevariabeln. Den här variabeln initieras till null.

Nu ska vi lägga till överlagringen av metoden Search som traverserar underkataloger och genererar båda eventen. Det enklaste sättet är att använda ett standardargument för att ange att du vill söka i alla kataloger:

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

Nu kan du köra programmet som anropar överlagringen för att söka i alla underkataloger. Det finns inga prenumeranter på den nya DirectoryChanged händelsen, men om du använder ?.Invoke() idiom ser du till att den fungerar korrekt.

Nu ska vi lägga till en hanterare för att skriva en rad som visar förloppet i konsolfönstret.

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

Du såg mönster som följs i hela .NET-ekosystemet. Genom att lära dig dessa mönster och konventioner skriver du snabbt idiomatiska C# och .NET.

Se även

Därefter visas några ändringar i dessa mönster i den senaste versionen av .NET.