Standardní vzory událostí .NET

Předchozí

Události .NET obecně dodržují několik známých vzorů. Standardizace těchto vzorů znamená, že vývojáři můžou využívat znalosti těchto standardních vzorů, které je možné použít pro jakýkoli program událostí .NET.

Pojďme si projít tyto standardní vzory, abyste měli všechny znalosti potřebné k vytvoření standardních zdrojů událostí a přihlášení k odběru a zpracování standardních událostí ve vašem kódu.

Podpisy delegáta události

Standardní podpis delegáta události .NET je:

void EventRaised(object sender, EventArgs args);

Návratový typ je neplatný. Události jsou založené na delegátech a jsou delegáty vícesměrového vysílání. To podporuje více odběratelů pro jakýkoli zdroj událostí. Jedna návratová hodnota z metody se nes škáluje na více odběratelů událostí. Jakou návratovou hodnotu zdroj události vidí po vyvolání události? Dále v tomto článku se dozvíte, jak vytvořit protokoly událostí, které podporují odběratele událostí, které hlásí informace do zdroje událostí.

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

Druhým argumentem je obvykle typ, který je odvozen z System.EventArgs. (V další části se dozvíte, že se tato konvence už nevynucuje.) Pokud váš typ události nepotřebuje žádné další argumenty, stále zadáte oba argumenty. Existuje zvláštní hodnota, kterou byste měli použít k označení, EventArgs.Empty že událost neobsahuje žádné další informace.

Pojďme vytvořit třídu, která uvádí soubory v adresáři nebo některé z jejích podadresářů, které následují podle vzoru. Tato komponenta vyvolá událost pro každý nalezený soubor, který odpovídá vzoru.

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

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

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

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

I když tento typ vypadá jako malý datový typ, měli byste postupovat podle konvence a nastavit ho jako odkaz (class). To znamená, že objekt argumentu bude předán odkazem a všechny aktualizace dat budou zobrazeny všemi odběrateli. První verze je neměnný objekt. Měli byste raději nastavit vlastnosti v typu argumentu události jako neměnné. Tímto způsobem nemůže jeden odběratel změnit hodnoty předtím, než je uvidí jiný odběratel. (Existují výjimky, jak vidíte níže.)

Dále musíme vytvořit deklaraci události ve třídě FileSearcher. EventHandler<T> Využití typu znamená, že nemusíte vytvářet další definici typu. Jednoduše použijete obecnou specializaci.

Pojďme vyplnit FileSearcher třídy hledat 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))
        {
            RaiseFileFound(file);
        }
    }
    
    private void RaiseFileFound(string file) =>
        FileFound?.Invoke(this, new FileFoundArgs(file));
}

Definování a vyvolání událostí podobných polím

Nejjednodušším způsobem, jak přidat událost do 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 vypadalo jako špatný objektově orientovaný postup. Chcete chránit přístup k datům prostřednictvím vlastností nebo metod. I když to může vypadat jako špatný postup, kód vygenerovaný kompilátorem vytváří obálky, aby k objektům událostí bylo možné přistupovat pouze bezpečnými způsoby. Jedinými operacemi dostupnými u události podobné poli jsou obslužná rutina přidání:

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

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

fileLister.FileFound += onFileFound;

a odeberte obslužnou rutinu:

fileLister.FileFound -= onFileFound;

Všimněte si, že pro obslužnou rutinu existuje místní proměnná. Pokud jste použili tělo lambda, odebrání by nefungovalo správně. Jedná se o jinou instanci 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.

Vrácení hodnot od odběratelů událostí

Vaše jednoduchá verze funguje dobře. Pojďme přidat další funkci: Zrušení.

Při vyvolání nalezené události by naslouchací procesy měly být schopny zastavit další zpracování, pokud je tento soubor posledním požadovaným souborem.

Obslužné rutiny událostí nevrací hodnotu, takže je třeba ji komunikovat jiným způsobem. Standardní vzor události používá EventArgs objekt k zahrnutí polí, která můžou odběratelé událostí použít ke komunikaci.

Na základě sémantiky smlouvy Cancel je možné použít dva různé vzory. V obou případech přidáte do události EventArguments nalezené události logické pole.

Jeden vzor by umožnil každému odběrateli operaci zrušit. Pro tento vzor je nové pole inicializováno na false. Každý odběratel ho může změnit na true. Jakmile všichni odběratelé viděli vyvolání události, komponenta FileSearcher prozkoumá logickou hodnotu a provede akci.

Druhý model by operaci zrušil pouze v případě, že všichni předplatitelé chtěli operaci zrušit. V tomto vzoru se nové pole inicializuje tak, aby indikuje, že by operace měla být zrušena, a každý odběratel by ho mohl změnit tak, aby indikuje, že operace by měla pokračovat. Jakmile všichni odběratelé viděli událost vyvolání, komponenta FileSearcher zkontroluje logickou hodnotu a provede akci. V tomto vzoru je ještě jeden další krok: komponenta musí vědět, jestli se událost zobrazila některým odběratelům. Pokud nejsou žádní odběratelé, pole by značilo nesprávné zrušení.

Pojďme pro tuto ukázku implementovat první verzi. Do typu je potřeba přidat logické pole s názvem CancelRequested FileFoundArgs :

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 falsevýchozí hodnotu Boolean pole, takže ho nezrušíte omylem. Jedinou další změnou komponenty je zkontrolovat příznak po vyvolání události a zjistit, jestli některý z odběratelů požádal o zrušení:

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

Jednou z výhod tohoto modelu je, že se nejedná o zásadní změnu. Nikdo z odběratelů předtím nepožádal o zrušení a stále ještě nejsou. Žádný kód odběratele není potřeba aktualizovat, pokud nechce podporovat nový protokol zrušení. Je to velmi volně párované.

Pojďme odběratele aktualizovat tak, aby po nalezení prvního spustitelného souboru požádá o 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 jednu další funkci a předvést další idiomy jazyka pro události. Pojďme přidat přetížení Search metody, která prochází všechny podadresáře při hledání souborů.

To může být zdlouhavá operace v adresáři s mnoha podadresáři. Pojďme přidat událost, která se vyvolá při zahájení každého hledání v novém adresáři. To umožňuje odběratelům sledovat průběh a aktualizovat uživatele podle průběhu. Všechny ukázky, které jste zatím vytvořili, jsou veřejné. Pojďme to udělat jako interní událost. To znamená, že můžete také použít typy pro argumenty interní.

Začnete vytvořením nové odvozené třídy EventArgs pro generování sestav nového adresáře a průběhu.

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

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

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

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

Kód, který tady napíšete, zrcadlí kód, který kompilátor generuje pro definice událostí pole, které jste viděli dříve. Událost vytvoříte pomocí syntaxe velmi podobné události, která se používá pro vlastnosti. Všimněte si, že obslužné rutiny mají různé názvy: add a remove. Volají se k odběru události nebo se z události odhlásí. Všimněte si, že musíte také deklarovat privátní backing pole pro uložení proměnné události. Inicializuje se na hodnotu null.

V dalším kroku přidáme přetížení Search metody, která prochází podadresáře a vyvolává obě události. Nejjednodušším způsobem, jak toho dosáhnout, je použít výchozí argument k určení, že chcete prohledat 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)
        {
            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;
}

V tomto okamžiku můžete spustit aplikaci, která volá přetížení pro vyhledávání všech podadresér. V nové DirectoryChanged události nejsou žádní odběratelé, ale použití ?.Invoke() idiomu zajišťuje, že to 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é se používají v ekosystému .NET. Když se seznámíte s těmito vzory a konvencemi, budete rychle psát idiomaticky C# a .NET.

Viz také

V dalším kroku uvidíte některé změny v těchto vzorech v nejnovější verzi .NET.