Megosztás a következőn keresztül:


Standard .NET-eseményminták

Előző

A .NET-események általában néhány ismert mintát követnek. Ezeknek a mintáknak a szabványosítása azt jelenti, hogy a fejlesztők felhasználhatják ezeket a standard mintákat, amelyek bármely .NET-eseményprogramra alkalmazhatók.

Tekintsük át ezeket a standard mintákat, hogy minden olyan tudás birtokában legyen, amellyel szabványos eseményforrásokat hozhat létre, és előfizethet és feldolgozhat standard eseményeket a kódban.

Eseménymeghatalmazási aláírások

A .NET-eseménydelegáltak szabványos aláírása a következő:

void EventRaised(object sender, EventArgs args);

A visszatérési típus érvénytelen. Az események meghatalmazottakon alapulnak, és csoportos küldésű meghatalmazottak. Ez több előfizetőt támogat minden eseményforráshoz. A metódus egyetlen visszatérési értéke nem skálázható több esemény-előfizetőre. Melyik visszatérési értéket látja az eseményforrás az esemény létrehozása után? A cikk későbbi részében megtudhatja, hogyan hozhat létre olyan eseményprotokollokat, amelyek támogatják az esemény-előfizetőket, amelyek információkat jelentenek az eseményforrásnak.

Az argumentumlista két argumentumot tartalmaz: a feladót és az eseményargumentumokat. A fordítási idő típusa sender annak ellenére, System.Objecthogy valószínűleg ismer egy olyan származtatott típust, amely mindig helyes lenne. Konvenció szerint használja object.

A második argumentum általában egy olyan típus volt, amely a következőből System.EventArgsszármazik: . (A következő szakaszban láthatja, hogy ez a konvenció már nincs kényszerítve.) Ha az eseménytípushoz nincs szükség további argumentumokra, akkor is mindkét argumentumot meg kell adnia. Van egy speciális érték, amelyet arra kell használnia, EventArgs.Empty hogy az esemény ne tartalmazzon további információkat.

Hozzunk létre egy osztályt, amely egy könyvtárban lévő fájlokat vagy annak bármely alkönyvtárát listázza, amely egy mintát követ. Ez az összetevő minden talált fájlhoz létrehoz egy eseményt, amely megfelel a mintának.

Az eseménymodellek használata néhány tervezési előnyt biztosít. Több eseményfigyelőt is létrehozhat, amelyek különböző műveleteket hajtanak végre egy keresett fájl megtalálásakor. A különböző figyelők kombinálásával robusztusabb algoritmusokat hozhat létre.

Itt található a keresett fájl megkeresésére vonatkozó kezdeti eseményargumentum-deklaráció:

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

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

Annak ellenére, hogy ez a típus kicsi, csak adattípusnak tűnik, kövesse az egyezményt, és legyen hivatkozási (class) típus. Ez azt jelenti, hogy az argumentumobjektumot hivatkozással továbbítja a rendszer, és az adatok minden frissítését az összes előfizető megtekinti. Az első verzió egy nem módosítható objektum. Az eseményargumentum tulajdonságai nem módosíthatók. Így az egyik előfizető nem módosíthatja az értékeket, mielőtt egy másik előfizető megtekintené őket. (Vannak kivételek erre, ahogy alább látható.)

Ezután létre kell hoznunk az eseménydeklarációt a FileSearcher osztályban. A típus kihasználása EventHandler<T> azt jelenti, hogy nem kell még egy típusdefiníciót létrehoznia. Egyszerűen csak általános specializációt használ.

Töltsük ki a FileSearcher osztályt egy mintának megfelelő fájlok kereséséhez, és hozzuk létre a megfelelő eseményt egyezés észlelésekor.

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

Mezőszerű események definiálása és emelése

Az eseménynek az osztályhoz való hozzáadásának legegyszerűbb módja, ha az eseményt nyilvános mezőként deklarálja, ahogy az előző példában is látható:

public event EventHandler<FileFoundArgs>? FileFound;

Ez úgy tűnik, hogy egy nyilvános mezőt deklarál, ami rossz objektumorientált gyakorlatnak tűnik. Az adathozzáférést tulajdonságokon vagy metódusokon keresztül szeretné védeni. Bár ez rossz gyakorlatnak tűnhet, a fordító által létrehozott kód burkolókat hoz létre, így az eseményobjektumok csak biztonságos módon érhetők el. Egy mezőhöz hasonló eseményen csak a kezelő hozzáadása használható:

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

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

fileLister.FileFound += onFileFound;

és távolítsa el a kezelőt:

fileLister.FileFound -= onFileFound;

Vegye figyelembe, hogy a kezelőnek van egy helyi változója. Ha a lambda törzsét használta, az eltávolítás nem működik megfelelően. Ez a meghatalmazott egy másik példánya lenne, és csendben semmit sem tenne.

Az osztályon kívüli kód nem tudja elindítani az eseményt, és semmilyen más műveletet sem tud végrehajtani.

Esemény-előfizetők értékeinek visszaadása

Az egyszerű verzió jól működik. Adjunk hozzá egy másik funkciót: a lemondást.

A talált esemény létrehozásakor a figyelőknek le kell tudniuk állítani a további feldolgozást, ha ez a fájl az utolsó, amelyet keresnek.

Az eseménykezelők nem adnak vissza értéket, ezért ezt más módon kell közölnie. A standard eseményminta az EventArgs objektummal olyan mezőket tartalmaz, amelyeket az esemény-előfizetők a megszakítások kommunikációja során használhatnak.

A Mégse szerződés szemantikája alapján két különböző minta használható. Mindkét esetben logikai mezőt ad hozzá a talált fájlesemény EventArguments függvényéhez.

Egy minta lehetővé teszi, hogy bármely előfizető megszakítsa a műveletet. Ebben a mintában az új mező inicializálva lesz.false Bármely előfizető módosíthatja azt true. Miután az összes előfizető látta az eseményt, a FileSearcher összetevő megvizsgálja a logikai értéket, és végrehajtja a műveletet.

A második minta csak akkor szakítaná meg a műveletet, ha az összes előfizető le szeretné mondani a műveletet. Ebben a mintában az új mező inicializálva van, hogy jelezze a művelet megszakítását, és bármely előfizető módosíthatja, hogy a művelet folytatódjon. Miután az összes előfizető látta az eseményt, a FileSearcher összetevő megvizsgálja a logikai értéket, és végrehajtja a műveletet. Ebben a mintában van egy további lépés: az összetevőnek tudnia kell, hogy valamelyik előfizető látta-e az eseményt. Ha nincsenek előfizetők, a mező helytelen lemondást jelez.

Implementáljuk a minta első verzióját. A típushoz hozzá kell adnia egy logikai mezőtCancelRequested:FileFoundArgs

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

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

Ez az új mező automatikusan inicializálva falselesz egy mező alapértelmezett értékére Boolean , így nem szakítja meg véletlenül. Az összetevő egyetlen másik módosítása az, hogy az esemény felemelése után ellenőrizze a jelzőt, hogy az előfizetők bármelyike kérte-e a lemondást:

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

Ennek a mintának az egyik előnye, hogy ez nem törés változás. Egyik előfizető sem kérte korábban a lemondást, és még mindig nem. Az előfizetői kód egyikének sem kell frissítenie, hacsak nem szeretné támogatni az új megszakítási protokollt. Nagyon lazán össze van állítva.

Frissítsük az előfizetőt úgy, hogy az az első végrehajtható fájl megkeresése után kérje a lemondást:

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

Másik eseménydeklaráció hozzáadása

Adjunk hozzá még egy funkciót, és mutassuk be az események egyéb nyelvi kifejezéseit. Adjunk hozzá egy túlterhelést a Search metódushoz, amely minden alkönyvtárat bejár a fájlok keresése során.

Ez hosszú művelet lehet egy sok alkönyvtárat tartalmazó címtárban. Vegyünk fel egy eseményt, amely az egyes új címtárkeresések megkezdésekor merül fel. Így az előfizetők nyomon követhetik az előrehaladást, és frissítheti a felhasználót a folyamat előrehaladásáról. Az eddig létrehozott minták nyilvánosak. Tegyük ezt belső eseménysé. Ez azt jelenti, hogy az argumentumokhoz használt típusokat is belsővé teheti.

Először hozza létre az új EventArgs származtatott osztályt az új címtár és a folyamat jelentéséhez.

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

Ismét követheti a javaslatokat az eseményargumentumok megváltoztathatatlan hivatkozástípusának létrehozásához.

Ezután adja meg az eseményt. Ezúttal egy másik szintaxist fog használni. A mezőszintaxis használata mellett explicit módon is létrehozhatja a tulajdonságot kezelők hozzáadásával és eltávolításával. Ebben a mintában nem lesz szükség további kódra ezekben a kezelőkben, de ez bemutatja, hogyan hozhatja létre őket.

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

Az itt írt kód sokféleképpen tükrözi a fordító által a korábban látott mezőesemény-definíciókhoz létrehozott kódot. Az eseményt a tulajdonságokhoz használt szintaxishoz nagyon hasonló szintaxissal hozza létre. Figyelje meg, hogy a kezelőknek különböző neveik vannak: add és remove. Ezeket az eseményre való feliratkozásra vagy az eseményről való leiratkozásra hívjuk meg. Figyelje meg, hogy az eseményváltozó tárolásához egy privát háttérmezőt is deklarálnia kell. Null értékűre van inicializálva.

Ezután vizsgáljuk meg az alkönyvtárakat bejáró és mindkét eseményt felemésztő metódus túlterhelését Search . Ennek legegyszerűbb módja az, ha egy alapértelmezett argumentum használatával adja meg, hogy az összes könyvtárban szeretne keresni:

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

Ezen a ponton futtathatja az alkalmazást, amely meghívja a túlterhelést az összes alkönyvtár kereséséhez. Nincsenek előfizetők az új DirectoryChanged eseményen, de az ?.Invoke() idiómával biztosítható, hogy ez megfelelően működjön.

Adjunk hozzá egy kezelőt egy olyan sor írásához, amely a konzolablak előrehaladását mutatja.

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

A .NET-ökoszisztémában követett mintákat láthatta. Ezeknek a mintáknak és konvencióknak a megismerésével gyorsan idiomatikus C# és .NET írást fog írni.

Lásd még

A következő lépésben a .NET legújabb kiadásában láthat néhány változást ezekben a mintákban.