Delen via


Standaard .NET-gebeurtenispatronen

Vorige

.NET-gebeurtenissen volgen over het algemeen enkele bekende patronen. Standaardiseren op deze patronen betekent dat ontwikkelaars kennis kunnen maken van deze standaardpatronen, die kunnen worden toegepast op elk .NET-gebeurtenisprogramma.

Laten we deze standaardpatronen doorlopen, zodat u alle kennis hebt die u nodig hebt om standaard gebeurtenisbronnen te maken en standaard gebeurtenissen in uw code te abonneren en te verwerken.

Handtekeningen voor gebeurtenisdelegering

De standaardhandtekening voor een .NET-gebeurtenisdelegatie is:

void EventRaised(object sender, EventArgs args);

Het retourtype is ongeldig. Gebeurtenissen zijn gebaseerd op gemachtigden en multicast-gemachtigden. Dat ondersteunt meerdere abonnees voor elke gebeurtenisbron. De enkele retourwaarde van een methode wordt niet geschaald naar meerdere gebeurtenisabonnees. Welke retourwaarde ziet de gebeurtenisbron na het genereren van een gebeurtenis? Verderop in dit artikel ziet u hoe u gebeurtenisprotocollen maakt die ondersteuning bieden voor gebeurtenisabonnees die informatie rapporteren aan de gebeurtenisbron.

De argumentenlijst bevat twee argumenten: de afzender en de gebeurtenisargumenten. Het type compileertijd sender is System.Object, ook al weet u waarschijnlijk een meer afgeleid type dat altijd correct zou zijn. Gebruik volgens conventie object.

Het tweede argument is meestal een type dat is afgeleid van System.EventArgs. (In de volgende sectie ziet u dat deze conventie niet meer wordt afgedwongen.) Als uw gebeurtenistype geen extra argumenten nodig heeft, geeft u nog steeds beide argumenten op. Er is een speciale waarde die EventArgs.Empty u moet gebruiken om aan te geven dat uw gebeurtenis geen aanvullende informatie bevat.

We gaan een klasse bouwen waarin bestanden in een map of een van de bijbehorende submappen worden vermeld die een patroon volgen. Dit onderdeel genereert een gebeurtenis voor elk bestand dat overeenkomt met het patroon.

Het gebruik van een gebeurtenismodel biedt enkele ontwerpvoordelen. U kunt meerdere gebeurtenislisteners maken die verschillende acties uitvoeren wanneer een gezocht bestand wordt gevonden. Door de verschillende listeners te combineren, kunnen robuustere algoritmen worden gemaakt.

Hier volgt de declaratie van het eerste gebeurtenisargument voor het vinden van een gezocht bestand:

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

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

Hoewel dit type eruitziet als een klein, alleen gegevenstype, moet u de conventie volgen en een verwijzingstype (class) maken. Dit betekent dat het argumentobject wordt doorgegeven via een verwijzing en dat alle updates van de gegevens door alle abonnees worden weergegeven. De eerste versie is een onveranderbaar object. U moet de eigenschappen in uw gebeurtenisargumenttype onveranderbaar maken. Op die manier kan één abonnee de waarden niet wijzigen voordat een andere abonnee deze ziet. (Er zijn uitzonderingen op dit, zoals hieronder wordt weergegeven.)

Vervolgens moeten we de gebeurtenisdeclaratie maken in de klasse FileSearcher. Als u gebruikmaakt van het EventHandler<T> type, hoeft u geen nieuwe typedefinitie te maken. U gebruikt gewoon een algemene specialisatie.

Laten we de FileSearcher-klasse invullen om te zoeken naar bestanden die overeenkomen met een patroon en de juiste gebeurtenis te genereren wanneer een overeenkomst wordt gedetecteerd.

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

Veldachtige gebeurtenissen definiëren en genereren

De eenvoudigste manier om een gebeurtenis aan uw klasse toe te voegen, is door die gebeurtenis als een openbaar veld te declareren, zoals in het vorige voorbeeld:

public event EventHandler<FileFoundArgs>? FileFound;

Dit lijkt erop dat het een openbaar veld declareren, wat een slechte objectgeoriënteerde praktijk lijkt te zijn. U wilt gegevenstoegang beveiligen via eigenschappen of methoden. Hoewel dit kan lijken op een slechte gewoonte, maakt de code die door de compiler wordt gegenereerd wrappers, zodat de gebeurtenisobjecten alleen op veilige manieren kunnen worden geopend. De enige bewerkingen die beschikbaar zijn voor een veldachtige gebeurtenis zijn handler toevoegen:

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

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

fileLister.FileFound += onFileFound;

en verwijder handler:

fileLister.FileFound -= onFileFound;

Houd er rekening mee dat er een lokale variabele is voor de handler. Als u de body van de lambda hebt gebruikt, werkt het verwijderen niet goed. Het zou een ander exemplaar van de gemachtigde zijn en op de achtergrond niets doen.

Code buiten de klasse kan de gebeurtenis niet genereren en kan ook geen andere bewerkingen uitvoeren.

Waarden retourneren van gebeurtenisabonnees

Uw eenvoudige versie werkt prima. Laten we nog een functie toevoegen: Annulering.

Wanneer u de gevonden gebeurtenis opheegt, moeten listeners verdere verwerking kunnen stoppen als dit bestand de laatste is die is gezocht.

De gebeurtenis-handlers retourneren geen waarde, dus u moet dat op een andere manier communiceren. Het standaard gebeurtenispatroon maakt gebruik van het EventArgs object om velden op te nemen die gebeurtenisabonnees kunnen gebruiken om te communiceren met annuleren.

Er kunnen twee verschillende patronen worden gebruikt op basis van de semantiek van het contract Annuleren. In beide gevallen voegt u een booleaans veld toe aan de EventArguments voor de gevonden bestandsgebeurtenis.

Met één patroon kan elke abonnee de bewerking annuleren. Voor dit patroon wordt het nieuwe veld geïnitialiseerd naar false. Elke abonnee kan deze wijzigen in true. Nadat alle abonnees de gebeurtenis hebben gegenereerd, onderzoekt het onderdeel FileSearcher de booleaanse waarde en voert de actie uit.

Het tweede patroon annuleert alleen de bewerking als alle abonnees de bewerking willen annuleren. In dit patroon wordt het nieuwe veld geïnitialiseerd om aan te geven dat de bewerking moet worden geannuleerd en kan elke abonnee dit wijzigen om aan te geven dat de bewerking moet worden voortgezet. Nadat alle abonnees de gebeurtenis hebben gezien die is gegenereerd, onderzoekt het filesearcher-onderdeel de Booleaanse waarde en voert de actie uit. Er is een extra stap in dit patroon: het onderdeel moet weten of abonnees de gebeurtenis hebben gezien. Als er geen abonnees zijn, geeft het veld een onjuiste annulering aan.

Laten we de eerste versie voor dit voorbeeld implementeren. U moet een booleaans veld met de naam CancelRequested toevoegen aan het FileFoundArgs type:

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

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

Dit nieuwe veld wordt automatisch geïnitialiseerd, falsede standaardwaarde voor een Boolean veld, zodat u niet per ongeluk annuleert. De enige andere wijziging in het onderdeel is het controleren van de vlag na het indienen van de gebeurtenis om te zien of een van de abonnees een annulering heeft aangevraagd:

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

Een voordeel van dit patroon is dat het geen belangrijke wijziging is. Geen van de abonnees heeft eerder om annulering gevraagd en ze zijn nog steeds niet. Geen van de abonneecode moet worden bijgewerkt, tenzij ze het nieuwe annuleringsprotocol willen ondersteunen. Het is heel losjes gekoppeld.

We gaan de abonnee bijwerken zodat deze een annulering aanvraagt zodra het het eerste uitvoerbare bestand heeft gevonden:

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

Een andere gebeurtenisdeclaratie toevoegen

Laten we nog een functie toevoegen en andere taalidiomen voor gebeurtenissen demonstreren. Laten we een overbelasting toevoegen van de Search methode die alle submappen doorkruist in het zoeken naar bestanden.

Dit kan een lange bewerking in een map met veel submappen zijn. Laten we een gebeurtenis toevoegen die wordt gegenereerd wanneer elke nieuwe directoryzoekopdracht begint. Hierdoor kunnen abonnees de voortgang bijhouden en de gebruiker bijwerken naar de voortgang. Alle voorbeelden die u tot nu toe hebt gemaakt, zijn openbaar. Laten we deze een interne gebeurtenis maken. Dat betekent dat u ook de typen kunt maken die worden gebruikt voor de argumenten intern.

U begint met het maken van de nieuwe afgeleide klasse EventArgs voor het rapporteren van de nieuwe map en de voortgang.

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

Nogmaals, u kunt de aanbevelingen volgen om een onveranderbaar verwijzingstype te maken voor de gebeurtenisargumenten.

Definieer vervolgens de gebeurtenis. Deze keer gebruikt u een andere syntaxis. Naast het gebruik van de veldsyntaxis kunt u de eigenschap expliciet maken, met handlers voor toevoegen en verwijderen. In dit voorbeeld hebt u geen extra code in deze handlers nodig, maar dit laat zien hoe u ze zou maken.

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

Op veel manieren weerspiegelt de code die u hier schrijft de code die de compiler genereert voor de veld gebeurtenisdefinities die u eerder hebt gezien. U maakt de gebeurtenis met behulp van syntaxis die vergelijkbaar is met de syntaxis die wordt gebruikt voor eigenschappen. U ziet dat de handlers verschillende namen hebben: add en remove. Deze worden aangeroepen om u te abonneren op de gebeurtenis of om u af te melden voor de gebeurtenis. U moet ook een privé-backingveld declareren om de gebeurtenisvariabele op te slaan. Deze wordt geïnitialiseerd op null.

Vervolgens gaan we de overbelasting toevoegen van de Search methode die door submappen gaat en beide gebeurtenissen genereert. De eenvoudigste manier om dit te bereiken, is door een standaardargument te gebruiken om op te geven dat u in alle mappen wilt zoeken:

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

Op dit moment kunt u de toepassing uitvoeren die de overbelasting aanroept voor het doorzoeken van alle submappen. Er zijn geen abonnees op de nieuwe DirectoryChanged gebeurtenis, maar het gebruik van de ?.Invoke() idiom zorgt ervoor dat dit correct werkt.

Laten we een handler toevoegen om een regel te schrijven waarin de voortgang in het consolevenster wordt weergegeven.

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

U hebt patronen gezien die in het .NET-ecosysteem worden gevolgd. Door deze patronen en conventies te leren, schrijft u snel idiomatische C# en .NET.

Zie ook

Vervolgens ziet u enkele wijzigingen in deze patronen in de meest recente versie van .NET.