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 dra nytta av kunskap om dessa standardmönster, som kan tillämpas på alla .NET-händelseprogram.

Nu ska vi gå igenom de här standardmönstren så att du får all kunskap du behöver för att skapa standardhändelsekällor och prenumerera 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);

Returtypen är ogiltig. Händelser baseras på ombud och är multicast-ombud. Det stöder flera prenumeranter för alla händelsekällor. Det enkla returvärdet från en metod skalas inte till flera händelseprenumeranter. Vilket returvärde ser händelsekällan när en händelse har skapats? Senare i den här artikeln får du se hur du skapar händelseprotokoll som stöder händelseprenumeranter som rapporterar information till händelsekällan.

Argumentlistan innehåller två argument: avsändaren och händelseargumenten. 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 .

Det andra argumentet har vanligtvis varit en typ som härleds från System.EventArgs. (I nästa avsnitt ser du att den här konventionen inte längre tillämpas.) Om händelsetypen inte behöver några ytterligare argument anger du fortfarande båda argumenten. Det finns ett särskilt värde som EventArgs.Empty 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 inledande 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 med 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 detta, som du ser nedan.)

Sedan måste vi skapa händelsedeklarationen i klassen FileSearcher. Om du använder typen EventHandler<T> 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))
        {
            RaiseFileFound(file);
        }
    }
    
    private void RaiseFileFound(string file) =>
        FileFound?.Invoke(this, new FileFoundArgs(file));
}

Definiera och skapa 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 detta kan se ut som en dålig metod skapar koden som genereras av kompilatorn omslutning så att händelseobjekten bara kan nås på säkra sätt. De enda åtgärder som är tillgängliga för en fältliknande händelse är tilläggshanteraren:

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

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

fileLister.FileFound += onFileFound;

och ta bort hanteraren:

fileLister.FileFound -= onFileFound;

Observera att det finns en lokal variabel för hanteraren. Om du använde lambda-kroppen skulle borttagningen 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.

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 den hittade händelsen 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 alla prenumeranter har sett händelsen höjas 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 sett händelsen höjas undersöker FileSearcher-komponenten det booleska objektet och vidtar åtgärder. Det finns ett extra steg i det här mönstret: komponenten måste veta om några prenumeranter har sett 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 nya fältet initieras automatiskt till false, standardvärdet för ett Boolean fält, 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 har begärt en annullering:

private void SearchDirectory(string directory, string searchPattern)
{
    foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
    {
        FileFoundArgs args = RaiseFileFound(file);
        if (args.CancelRequested)
        {
            break;
        }
    }
}

private FileFoundArgs RaiseFileFound(string file)
{
    var args = new FileFoundArgs(file);
    FileFound?.Invoke(this, args);
    return args;
}

En fördel med det här mönstret är att det inte är en icke-bakåtkompatibel ändring. Ingen av prenumeranterna begärde annullering tidigare, och det är de fortfarande inte. Ingen av prenumerantkoden behöver uppdateras om de inte vill ha stöd för det nya avbryt-protokollet. Det är väldigt löst kopplat.

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

Nu ska vi lägga till ytterligare en funktion och demonstrera andra språk-idiom för händelser. Nu ska vi lägga till en överlagring av metoden Search som passerar alla underkataloger på jakt efter filer.

Det 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. Detta 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 till en intern händelse. Det innebär att du också kan göra de typer som används för argumenten 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 egenskapen 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 har sett tidigare. Du skapar händelsen med syntax som liknar den som används för egenskaper. Observera att hanterarna har olika namn: add och remove. Dessa 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 initieras till null.

Nu ska vi lägga till överlagringen av metoden Search som passerar underkataloger och genererar båda händelserna. Det enklaste sättet att åstadkomma detta ä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)
        {
            RaiseSearchDirectoryChanged(dir, totalDirs, completedDirs++);
            // Search 'dir' and its subdirectories for files that match the search pattern:
            SearchDirectory(dir, searchPattern);
        }
        // Include the Current Directory:
        RaiseSearchDirectoryChanged(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))
    {
        FileFoundArgs args = RaiseFileFound(file);
        if (args.CancelRequested)
        {
            break;
        }
    }
}

private void RaiseSearchDirectoryChanged(
    string directory, int totalDirs, int completedDirs) =>
    _directoryChanged?.Invoke(
        this,
            new SearchDirectoryArgs(directory, totalDirs, completedDirs));

private FileFoundArgs RaiseFileFound(string file)
{
    var args = new FileFoundArgs(file);
    FileFound?.Invoke(this, args);
    return args;
}

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 med hjälp av formspråket ?.Invoke() ser du till att detta 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 har sett mönster som följs i hela .NET-ekosystemet. Genom att lära dig dessa mönster och konventioner kommer du snabbt att skriva idiomatiska C# och .NET.

Se även

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