Standardní vzory událostí .NET

Předchozí

Události .NET obecně následují několik známých vzorů. Standardizace těchto vzorů znamená, že vývojáři mohou využít znalosti těchto standardních vzorů, které lze použít na jakýkoli program událostí .NET.

Pojďme si projít tyto standardní vzory, abyste měli všechny znalosti, které potřebujete k vytvoření standardních zdrojů událostí, a přihlásit se k odběru a zpracovávat standardní události v kódu.

Signatury delegátů událostí

Standardní signatura pro delegáta události .NET je:

void OnEventRaised(object sender, EventArgs args);

Návratový typ je void. Události jsou založené na delegátech a jsou delegáti vícesměrového vysílání. Který podporuje více předplatitelů pro libovolný zdroj události. Jediná návratová hodnota z metody se neškáluje na více odběratelů událostí. Jakou návratovou hodnotu zdroj události uvidí po vyvolání události? Později v tomto článku se dozvíte, jak vytvořit protokoly událostí, které podporují předplatitele událostí, které oznamují informace do zdroje událostí.

Seznam argumentů obsahuje dva argumenty: odesílatel a argumenty události. Typ doby kompilace sender je System.Object , i když pravděpodobně znáte více odvozeného typu, který by byl vždy správný. Podle konvence použijte object .

Druhý argument byl obvykle typ, který je odvozen z System.EventArgs . (Uvidíte v Další části , že tato konvence už není vynutila.) Pokud váš typ události nepotřebuje žádné další argumenty, budete přesto poskytovat oba argumenty. K dispozici je speciální hodnota, EventArgs.Empty kterou byste měli použít k označení, že vaše událost neobsahuje žádné další informace.

Pojďme sestavit třídu, která obsahuje seznam souborů v adresáři, nebo některé z jeho podadresářů, které následují vzor. Tato komponenta vyvolá událost pro každý nalezený soubor, který odpovídá vzoru.

Použití modelu událostí poskytuje některé výhody návrhu. Můžete vytvořit několik posluchačů událostí, které při nalezení hledaného souboru provádějí různé akce. Kombinování různých posluchačů může vytvářet robustnější algoritmy.

Tady je počáteční deklarace argumentu události pro vyhledání hledaného souboru:

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

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

I když tento typ vypadá jako malý, pouze datový typ, měli byste postupovat podle konvence a vytvořit z něj odkaz ( class ) typ. To znamená, že objekt argumentu bude předán odkazem a všechny aktualizace dat budou zobrazeny všemi předplatiteli. První verze je neproměnlivý objekt. Měli byste raději nastavit vlastnosti v argumentu události typ unmutableed. Jeden předplatitel pak nemůže změnit hodnoty předtím, než je uvidí jiný odběratel. (V takovém případě jsou k dispozici výjimky, jak vidíte níže.)

Dále je potřeba vytvořit deklaraci události ve třídě hledání ve službě. Využití EventHandler<T> typu znamená, že ještě nemusíte vytvářet jinou definici typu. Jednoduše použijete obecnou specializaci.

Pojďme vyplňovat třídu hledání souborů, aby bylo možné vyhledávat soubory, které odpovídají vzoru, a vyvolat správnou událost při zjištění shody.

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

Definování a vyvolávání událostí Field-Like

Nejjednodušší způsob, jak přidat událost do vaší třídy, je deklarovat tuto událost jako veřejné pole, jako v předchozím příkladu:

public event EventHandler<FileFoundArgs> FileFound;

Vypadá to, že deklaruje veřejné pole, což by znamenalo špatný objektově orientovaný postup. Chcete chránit přístup k datům prostřednictvím vlastností nebo metod. Přestože to může vypadat jako špatný postup, kód generovaný kompilátorem vytvoří obálky, aby k objektům událostí bylo možné získávat přístup pouze bezpečným způsobem. Jediné operace dostupné v události podobné poli jsou přidání obslužné rutiny:

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

fileLister.FileFound += onFileFound;

a odebrat obslužnou rutinu:

fileLister.FileFound -= onFileFound;

Všimněte si, že pro obslužnou rutinu je k dispozici místní proměnná. Pokud jste použili tělo lambda, odebrání by nefungovalo správně. By to byla jiná instance delegáta a tiše nedělá nic.

Kód mimo třídu nemůže vyvolat událost, ani nemůže provádět žádné jiné operace.

Vracení hodnot od odběratelů událostí

Vaše jednoduchá verze funguje správně. Pojďme přidat další funkci: zrušení.

Pokud vyvoláte nalezenou událost, naslouchací procesy by měly být schopné zastavit další zpracování, pokud je tento soubor ten, který byl naposledy vyžádán.

Obslužné rutiny událostí nevrací hodnotu, takže je třeba komunikovat jiným způsobem. Standardní vzor události používá objekt EventArgs k zahrnutí polí, které mohou předplatitelé události použít ke sdělení zrušení.

Existují dva různé vzory, které by mohly být použity na základě sémantiky zrušení smlouvy. V obou případech přidáte pole Boolean do EventArguments pro událost nalezeného souboru.

Jeden ze vzorů umožní jednomu předplatiteli zrušit operaci. Pro tento model je nové pole inicializováno na false . Každý předplatitel ho může změnit na true . Poté, co všichni předplatitelé viděli vyvolanou událost, ověří Tato komponenta logickou hodnotu a provede akci.

Druhý vzor by tuto operaci zrušil jenom v případě, že všichni předplatitelé chtěli operaci zrušit. V tomto modelu se nové pole inicializuje, aby označovalo, že operace se má zrušit, a každý předplatitel ho může změnit, aby označoval, že operace by měla pokračovat. Poté, co všichni předplatitelé viděli vyvolanou událost, vyhledá součást hledání v Boolean logickou hodnotu a provede akci. Tento model obsahuje ještě jeden krok navíc: součást musí zjistit, jestli se událost objevila pro nějaké předplatitele. Pokud žádné předplatitelé neexistují, pole by znamenalo nesprávné zrušení.

Pojďme implementovat první verzi této ukázky. Je nutné přidat pole Boolean s názvem CancelRequested k FileFoundArgs typu:

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

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

Toto nové pole se automaticky inicializuje na false výchozí hodnotu pro pole Boolean, takže se nebudete moct omylem zrušit. Jedinou jinou změnou součásti je zkontrolování příznaku po vyvolání události, aby bylo možné zjistit, zda některý z předplatitelů požadoval zrušení:

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

Jednou z výhod tohoto vzoru je, že se nejedná o zásadní změnu. Žádný z předplatitelů požádal o zrušení před a ještě není. Žádný z kódů předplatitele není potřeba aktualizovat, pokud nechtějí podporovat nový protokol zrušení. Je velmi volně spojená.

Pojďme předplatitele aktualizovat, aby po nalezení prvního spustitelného souboru vyžádal zrušení.

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

Přidání další deklarace události

Pojďme přidat další funkci a předvést pro události idiomy další jazyky. Pojďme přidat přetížení Search metody, která projde všechny podadresáře při hledání souborů.

Může se jednat o zdlouhavou operaci v adresáři s mnoha podadresáři. Pojďme přidat událost, která se aktivuje při každém zahájení hledání nového adresáře. To umožňuje předplatitelům sledovat průběh a aktualizovat uživatele jako průběh. Všechny ukázky, které jste dosud vytvořili, jsou veřejné. Pojďme udělat tuto interní událost. To znamená, že můžete také nastavit typy používané pro interní argumenty.

Začnete vytvořením nové odvozené třídy EventArgs pro vytváření sestav nového adresáře a postupu.

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

Znovu můžete postupovat podle doporučení a nastavit pro argumenty události neměnný typ odkazu.

Dále definujte událost. Tentokrát použijete jinou syntaxi. Kromě použití syntaxe pole můžete explicitně vytvořit vlastnost s obslužnými rutinami přidat a odebrat. V této ukázce nebudete v těchto obslužných rutinách potřebovat další kód, ale ukazuje, jak byste je vytvořili.

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

V mnoha způsobech kód, který sem píšete, zrcadlí kód, který kompilátor generuje pro definice událostí pole, které jste viděli dříve. Vytvoříte událost pomocí syntaxe, která je velmi podobná jako vlastnost použitá pro vlastnosti. Všimněte si, že obslužné rutiny mají různé názvy: add a remove . Jsou volány k přihlášení k odběru události nebo k odhlášení odběru události. Všimněte si, že je také nutné deklarovat soukromé pole zálohování pro uložení proměnné události. Je inicializována na hodnotu null.

Nyní přidáme přetížení Search metody, která projde podadresáři a vyvolá obě události. Nejjednodušší způsob, jak to provést, je použít výchozí argument k určení, že chcete prohledávat všechny adresáře:

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++));
            // Search 'dir' and its subdirectories for files that match the search pattern:
            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;
    }
}

V tomto okamžiku můžete spustit aplikaci, která volá metodu Overload pro hledání všech podadresářů. K nové události neexistují žádní předplatitelé ChangeDirectory , ale pomocí ?.Invoke() idiom zajistí, že funguje správně.

Pojďme přidat obslužnou rutinu pro zápis řádku, který ukazuje průběh v okně konzoly.

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

Viděli jste vzory, které jsou následovány v rámci ekosystému .NET. Díky učení těchto vzorů a konvencí budete rychle psát idiomatickou C# a .NET.

Dále uvidíte některé změny v těchto vzorech v nejnovější verzi rozhraní .NET.

Další