Standardowe wzorce zdarzeń platformy .NET

Poprzednie

Zdarzenia platformy .NET zwykle są zgodne z kilkoma znanymi wzorcami. Standaryzacja tych wzorców oznacza, że deweloperzy mogą wykorzystać wiedzę na temat tych standardowych wzorców, które mogą być stosowane do dowolnego programu zdarzeń platformy .NET.

Zapoznajmy się z tymi standardowymi wzorcami, dzięki czemu uzyskasz całą wiedzę, którą musisz utworzyć standardowe źródła zdarzeń, oraz zasubskrybuj i przetwarzaj standardowe zdarzenia w kodzie.

Sygnatury delegatów zdarzeń

Standardowy podpis delegata zdarzeń platformy .NET to:

void EventRaised(object sender, EventArgs args);

Zwracany typ to void. Zdarzenia są oparte na delegatach i są delegatami multiemisji. Obsługuje to wielu subskrybentów dla dowolnego źródła zdarzeń. Pojedyncza zwracana wartość z metody nie jest skalowana do wielu subskrybentów zdarzeń. Która wartość zwracana jest widoczna w źródle zdarzeń po wystąpieniu zdarzenia? W dalszej części tego artykułu dowiesz się, jak tworzyć protokoły zdarzeń, które obsługują subskrybentów zdarzeń, którzy zgłaszają informacje do źródła zdarzeń.

Lista argumentów zawiera dwa argumenty: nadawcę i argumenty zdarzenia. Typ czasu kompilacji sender to System.Object, mimo że prawdopodobnie znasz bardziej pochodny typ, który zawsze będzie poprawny. Zgodnie z konwencją użyj polecenia object.

Drugi argument był zazwyczaj typem pochodzącym z System.EventArgsklasy . (W następnej sekcji zobaczysz, że ta konwencja nie jest już wymuszana). Jeśli typ zdarzenia nie wymaga żadnych dodatkowych argumentów, nadal będziesz dostarczać oba argumenty. Istnieje specjalna wartość, której należy użyć, aby określić, EventArgs.Empty że zdarzenie nie zawiera żadnych dodatkowych informacji.

Skompilujmy klasę, która wyświetla listę plików w katalogu lub dowolny z jego podkatalogów, które są zgodne ze wzorcem. Ten składnik zgłasza zdarzenie dla każdego znalezionego pliku zgodnego ze wzorcem.

Korzystanie z modelu zdarzeń zapewnia pewne korzyści projektowe. Można utworzyć wiele odbiorników zdarzeń, które wykonują różne akcje po znalezieniu szukanego pliku. Połączenie różnych odbiorników może tworzyć bardziej niezawodne algorytmy.

Oto początkowa deklaracja argumentu zdarzenia do znalezienia szukanego pliku:

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

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

Mimo że ten typ wygląda jak mały typ tylko dla danych, należy postępować zgodnie z konwencją i ustawić go jako odwołanie (class). Oznacza to, że obiekt argumentu zostanie przekazany przez odwołanie, a wszystkie aktualizacje danych będą wyświetlane przez wszystkich subskrybentów. Pierwsza wersja jest niezmiennym obiektem. Wolisz ustawić właściwości w typie argumentu zdarzenia niezmiennym. W ten sposób jeden subskrybent nie może zmienić wartości, zanim inny subskrybent zobaczy je. (Istnieją wyjątki od tego, jak zobaczysz poniżej).

Następnie musimy utworzyć deklarację zdarzenia w klasie FileSearcher. EventHandler<T> Wykorzystanie typu oznacza, że nie trzeba tworzyć jeszcze innej definicji typu. Po prostu używasz specjalizacji ogólnej.

Wypełnijmy klasę FileSearcher, aby wyszukać pliki pasujące do wzorca i podnieść prawidłowe zdarzenie po odnalezieniu dopasowania.

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

Definiowanie i wywoływanie zdarzeń przypominających pola

Najprostszym sposobem dodania zdarzenia do klasy jest zadeklarowanie tego zdarzenia jako pola publicznego, jak w poprzednim przykładzie:

public event EventHandler<FileFoundArgs>? FileFound;

Wygląda na to, że deklaruje pole publiczne, które wydaje się być złą praktyką zorientowaną na obiekt. Chcesz chronić dostęp do danych za pomocą właściwości lub metod. Chociaż może to wyglądać jak zła praktyka, kod wygenerowany przez kompilator tworzy otoki, dzięki czemu obiekty zdarzeń mogą być dostępne tylko w bezpieczny sposób. Jedyne operacje dostępne w zdarzeniu przypominającym pole to procedura obsługi dodawania:

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

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

fileLister.FileFound += onFileFound;

i usuń program obsługi:

fileLister.FileFound -= onFileFound;

Należy pamiętać, że istnieje zmienna lokalna dla programu obsługi. Jeśli użyto treści lambda, usunięcie nie będzie działać poprawnie. Byłoby to inne wystąpienie delegata i dyskretnie nic nie robić.

Kod spoza klasy nie może wywołać zdarzenia ani nie może wykonać żadnych innych operacji.

Zwracanie wartości od subskrybentów zdarzeń

Prosta wersja działa prawidłowo. Dodajmy kolejną funkcję: Anulowanie.

Po wywołaniu znalezionego zdarzenia odbiorniki powinny być w stanie zatrzymać dalsze przetwarzanie, jeśli ten plik jest ostatnim poszukiwanym.

Programy obsługi zdarzeń nie zwracają wartości, dlatego należy przekazać je w inny sposób. Standardowy wzorzec zdarzenia używa EventArgs obiektu do uwzględnienia pól, których subskrybenci zdarzeń mogą używać do komunikowania się z anulowaniem.

Można użyć dwóch różnych wzorców na podstawie semantyki kontraktu Anulowania. W obu przypadkach do zdarzenia znalezionego pliku zostanie dodane pole logiczne.

Jeden wzorzec umożliwiałby każdemu subskrybentowi anulowanie operacji. Dla tego wzorca nowe pole jest inicjowane na .false Każdy subskrybent może zmienić go na true. Po wystąpieniu zdarzenia przez wszystkich subskrybentów składnik FileSearcher sprawdza wartość logiczną i podejmuje działania.

Drugi wzorzec anuluje operację tylko wtedy, gdy wszyscy subskrybenci chcieli anulować operację. W tym wzorcu nowe pole jest inicjowane, aby wskazać, że operacja powinna zostać anulowana, a każdy subskrybent może go zmienić, aby wskazać, że operacja powinna być kontynuowana. Po wystąpieniu zdarzenia przez wszystkich subskrybentów składnik FileSearcher analizuje wartość logiczną i podejmuje działania. Ten wzorzec zawiera jeden dodatkowy krok: składnik musi wiedzieć, czy jakiś subskrybenci widzieli zdarzenie. Jeśli nie ma subskrybentów, pole będzie wskazywać niepoprawnie anulować.

Zaimplementujmy pierwszą wersję dla tego przykładu. Musisz dodać pole logiczne o nazwie CancelRequested do FileFoundArgs typu:

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

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

To nowe pole jest automatycznie inicjowane na false, wartość domyślna Boolean pola, więc nie anulujesz przypadkowo. Jedyną inną zmianą składnika jest sprawdzenie flagi po wyświetleniu zdarzenia, aby sprawdzić, czy którykolwiek z subskrybentów zażądał anulowania:

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

Jedną z zalet tego wzorca jest to, że nie jest to zmiana powodująca niezgodność. Żaden z subskrybentów nie zażądał wcześniej anulowania i nadal nie są. Żaden z kodu subskrybenta nie musi aktualizować, chyba że chce obsługiwać nowy protokół anulowania. Jest bardzo luźno powiązane.

Zaktualizujmy subskrybenta, aby żądał anulowania po znalezieniu pierwszego pliku wykonywalnego:

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

Dodawanie innej deklaracji zdarzenia

Dodajmy jeszcze jedną funkcję i demonstrujmy inne idiomy języka dla zdarzeń. Dodajmy przeciążenie Search metody, która przechodzi wszystkie podkatalogi w wyszukiwaniu plików.

Może to być długotrwała operacja w katalogu z wieloma podkatalogami. Dodajmy zdarzenie, które zostanie zgłoszone po rozpoczęciu każdego nowego wyszukiwania w katalogu. Dzięki temu subskrybenci mogą śledzić postęp i aktualizować użytkownika w miarę postępu. Wszystkie utworzone do tej pory przykłady są publiczne. Utwórzmy to zdarzenie wewnętrzne. Oznacza to, że można również tworzyć typy używane dla argumentów wewnętrznych.

Zaczniesz od utworzenia nowej klasy pochodnej EventArgs na potrzeby raportowania nowego katalogu i postępu.

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

Ponownie możesz postępować zgodnie z zaleceniami, aby ustawić niezmienny typ odwołania dla argumentów zdarzenia.

Następnie zdefiniuj zdarzenie. Tym razem użyjesz innej składni. Oprócz używania składni pola można jawnie utworzyć właściwość z funkcjami obsługi dodawania i usuwania. W tym przykładzie nie będziesz potrzebować dodatkowego kodu w tych programach obsługi, ale pokazuje to, jak można je utworzyć.

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

Na wiele sposobów kod, który piszesz tutaj, odzwierciedla kod generowany przez kompilator dla definicji zdarzeń pól, które zostały wcześniej wyświetlone. Zdarzenie jest tworzone przy użyciu składni bardzo podobnej do używanej dla właściwości. Zwróć uwagę, że programy obsługi mają różne nazwy: add i remove. Są one wywoływane w celu zasubskrybowania zdarzenia lub anulowania subskrypcji zdarzenia. Należy również zadeklarować prywatne pole zapasowe, aby przechowywać zmienną zdarzenia. Jest inicjowany na wartość null.

Następnie dodajmy przeciążenie Search metody, która przechodzi podkatalogi i wywołuje oba zdarzenia. Najprostszym sposobem osiągnięcia tego celu jest użycie domyślnego argumentu w celu określenia, czy chcesz przeszukać wszystkie katalogi:

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

W tym momencie możesz uruchomić aplikację wywołującą przeciążenie w celu wyszukania wszystkich podkataplików. W nowym DirectoryChanged zdarzeniu nie ma subskrybentów, ale użycie ?.Invoke() idiomu gwarantuje, że działa to poprawnie.

Dodajmy procedurę obsługi, aby napisać wiersz pokazujący postęp w oknie konsoli.

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

Widzieliśmy wzorce, które są obserwowane w całym ekosystemie platformy .NET. Ucząc się tych wzorców i konwencji, będziesz pisać idiomatyczne C# i .NET szybko.

Zobacz też

Następnie w najnowszej wersji platformy .NET zostaną wyświetlone pewne zmiany w tych wzorcach.