Sdílet prostřednictvím


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 použí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);

Tento standardizovaný podpis poskytuje informace o tom, kdy jsou události používány.

  • Návratový typ je neplatný. Události mohou mít nulový počet až mnoho posluchačů. Vyvolání události upozorní všechny posluchače. Obecně posluchači neposkytují hodnoty v reakci na události.
  • Události označují odesílatele: Podpis události obsahuje objekt, který událost vyvolal. To poskytuje jakémukoli posluchači mechanismus pro komunikaci s odesílatelem. Kompilovaný typ sender je System.Object, i když pravděpodobně znáte odvozenější typ, který by byl vždy správný. Podle konvence použijte object.
  • Události obsahují další informace v jedné struktuře: Parametr args je typ odvozený z System.EventArgs toho, který obsahuje další nezbytné informace. (Uvidíte v další části, že tato konvence už není vyžadována.) I když váš typ události nevyžaduje žádné další argumenty, stále musíte poskytnout oba argumenty. Existuje zvláštní hodnota, EventArgs.Empty byste měli použít k označení, že vaš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 posluchačů 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 je předán odkazem a všechny aktualizace dat jsou 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é. Jeden odběratel tak nemůže změnit hodnoty předtím, než je uvidí jiný odběratel. (V tomto postupu existují výjimky, jak vidíte později.)

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

Pojďme vyplnit třídu FileSearcher, a hledat soubory, které odpovídají vzoru, a vyvolat správnou událost, když je nalezena shoda.

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ání událostí typu pole

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ž tento kód může vypadat jako chybný 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é operace, které jsou k dispozici u události podobné poli, jsou přidání a odebrání obslužné rutiny:

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

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

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

Pro obslužný modul je místní proměnná. Pokud jste použili tělo lambda, remove obslužná rutina by nefungila správně. Bylo by to jiná instance delegáta a neprovedla by nic.

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

Od verze C# 14 lze události deklarovat jako částečné členy. Částečná deklarace události musí obsahovat definující deklaraci a implementující deklaraci. Definice deklarace musí používat syntaxi události podobné poli. Deklarace implementace musí uvádět obslužné rutiny add a remove.

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

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

Když vyvoláte událost Found, naslouchací procesy by měly být schopny zastavit další zpracování, pokud se jedná o poslední požadovaný soubor.

Obslužné rutiny událostí nevrací hodnotu, takže je potřeba ji vyjádřit jiným způsobem. Standardní vzor události používá objekt EventArgs pro zahrnutí polí, která mohou odběratelé událostí použít ke komunikaci zrušení.

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

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. Po vyvolání události pro všechny odběratele komponenta FileSearcher zkontroluje hodnotu typu boolean a provádí 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é zpracovávají vyvolanou událost, komponenta FileSearcher zkontroluje logickou hodnotu a provede akci. V tomto vzoru je ještě jeden krok navíc: komponenta musí vědět, jestli na událost odpověděli nějaký předplatitelé. Pokud nejsou žádní odběratelé, pole by značilo nesprávné zrušení.

Pojďme pro tuto ukázku implementovat první verzi. Do typu CancelRequested je potřeba přidat logické pole s názvem 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 false, 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))
    {
        var args = new FileFoundArgs(file);
        FileFound?.Invoke(this, args);
        if (args.CancelRequested)
            break;
    }
}

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 o něj nežádají. Žádný kód odběratele nevyžaduje aktualizace, pokud nechce podporovat nový protokol zrušení.

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. Přidejme přetíženou verzi Search metody pro procházení všech podadresářů při hledání souborů.

Tato metoda může být zdlouhavou operací 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. Tato událost umožňuje odběratelům sledovat průběh a aktualizovat uživatele podle průběhu. Všechny dosud vytvořené ukázky jsou veřejné. Pojďme tuto událost vytvořit jako interní událost. To znamená, že typy argumentů můžete také nastavit jako interní.

Začněte vytvořením nové odvozené třídy EventArgs pro oznamování nového adresáře a jeho 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 události přidáním a odebráním obslužných rutin. V této ukázce nepotřebujete v těchto obslužných rutinách 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;

Kód, který zde 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, která je podobná vlastnostem . Všimněte si, že obslužné rutiny mají různé názvy: add a remove. Tyto přístupové metody se používají k přihlášení nebo odhlášení události. Všimněte si, že musíte také deklarovat privátní backing pole pro uložení proměnné události. Tato proměnná se inicializuje na hodnotu null.

Teď přidejme přetížení Search metody, která prochází podadresáře a vyvolává obě události. Nejjednodušší způsob 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)
        {
            _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;
    }
}

V tuto chvíli můžete spustit aplikaci, která využívá přetíženou verzi pro vyhledávání všech podadresářů. V nové DirectoryChanged události nejsou žádní odběratelé, ale použití ?.Invoke() idiomu 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é následují v ekosystému .NET. Díky tomu, že se naučíte tyto vzory a konvence, píšete rychle idiomatické programování v jazyce C# a .NET.

Viz také

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