Udostępnij za pośrednictwem


Standardowe wzorce zdarzeń platformy .NET

Poprzednie

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

Przejdźmy przez te standardowe wzorce, abyś miał całą wiedzę potrzebną do tworzenia standardowych źródeł zdarzeń oraz subskrybowania i przetwarzania standardowych zdarzeń w swoim kodzie.

Sygnatury delegatów zdarzeń

Standardowy podpis delegata zdarzeń platformy .NET to:

void EventRaised(object sender, EventArgs args);

Ta standardowa sygnatura zapewnia wgląd, kiedy są używane zdarzenia:

  • Zwracany typ to pusty. Zdarzenia mogą mieć od zera do wielu nasłuchiwaczy. Wywołanie zdarzenia powiadamia wszystkich słuchaczy. Ogólnie rzecz biorąc, słuchacze nie dostarczają wartości w reakcji na zdarzenia.
  • Zdarzenia wskazująnadawcy: podpis zdarzenia zawiera obiekt, który zgłosił zdarzenie. Zapewnia to każdemu odbiornikowi mechanizm komunikowania się z nadawcą. 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.
  • Zdarzenia pakują więcej informacji w pojedynczej strukturze: args parametr jest typem pochodzącym z System.EventArgs, który zawiera wszelkie dodatkowe konieczne informacje. (W następnej sekcji zobaczysz, że ta konwencja nie jest już wymuszana). Jeśli typ zdarzenia nie wymaga więcej argumentów, nadal musisz podać oba argumenty. Istnieje specjalna wartość, EventArgs.Empty, której należy użyć, aby określić, ż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, jedynie danych typ, należy postępować zgodnie z konwencją i ustawić go jako typ referencyjny (class). Oznacza to, że obiekt argumentu jest przekazywany jako odniesienie, a wszystkie aktualizacje danych są widoczne dla wszystkich subskrybentów. Pierwsza wersja jest niezmiennym obiektem. Preferujesz, aby właściwości w typie argumentu zdarzenia były niezmienne. W ten sposób jeden subskrybent nie może zmienić wartości, zanim inny subskrybent je zobaczy. (Istnieją wyjątki od tej praktyki, jak widać później).

Następnie musimy utworzyć deklarację zdarzenia w klasie FileSearcher. Użycie typu System.EventHandler<TEventArgs> oznacza, że nie trzeba tworzyć jeszcze innej definicji typu. Wystarczy użyć ogólnej specjalizacji.

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))
        {
            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 deklarowanie pola publicznego jest sprzeczne z zasadami programowania obiektowego. Chcesz chronić dostęp do danych za pomocą właściwości lub metod. Chociaż ten kod może wyglądać jak zła praktyka, kod wygenerowany przez kompilator tworzy opakowania, co zapewnia, że obiekty zdarzeń mogą być dostępne tylko w bezpieczny sposób. Jedynymi operacjami dostępnymi w zdarzeniu przypominającym pole są dodawanie i usuwanie procedury obsługi:

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

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

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

Istnieje zmienna lokalna dla obsługi. Jeśli użyto treści lambda, remove procedura obsługi nie działałaby poprawnie. Byłoby to inne wystąpienie delegata i dyskretnie nic nie robić.

Kod poza klasą nie może podnieść zdarzenia ani nie może wykonywać żadnych innych operacji.

Począwszy od języka C# 14, zdarzenia mogą być deklarowane jako częściowe elementy członkowskie. Częściowa deklaracja zdarzenia musi zawierać deklarację definiującą i deklarację implementowania. Deklaracja definiująca musi używać składni zdarzenia przypominającego pole. Deklaracja implementacji musi zadeklarować procedury obsługi add i remove.

Zwracanie wartości od subskrybentów zdarzeń

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

Po wywołaniu zdarzenia Found 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 zawarcia pól, których subskrybenci zdarzeń mogą używać do komunikowania o anulowaniu.

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

Jeden wzorzec umożliwiałby każdemu subskrybentowi anulowanie operacji. Dla tego wzorca nowe pole jest inicjowane do false. Każdy subskrybent może zmienić go na true. Po podniesieniu zdarzenia dla 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 przetworzeniu zdarzenia przez wszystkich subskrybentów składnik FileSearcher sprawdza wartość logiczną i podejmuje działania. Ten wzorzec zawiera jeden dodatkowy krok: składnik musi wiedzieć, czy subskrybenci zareagowali na zdarzenie. Jeśli nie ma subskrybentów, pole mogłoby wskazywać anulowanie błędnie.

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 ustawiane na false, abyś nie anulował tego przypadkowo. Jedyną inną zmianą w komponencie jest sprawdzenie flagi po wywołaniu zdarzenia, czy którykolwiek z subskrybentów zażądał anulowania.

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

Jedną z zalet tego wzorca jest to, że nie jest to zmiana powodująca niezgodność. Żaden z subskrybentów wcześniej nie zażądał anulowania i nadal tego nie robi. Żaden kod subskrybenta nie wymaga aktualizacji, chyba że chce obsługiwać nowy protokół anulowania.

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 zaprezentujmy inne idiomy językowe dotyczące zdarzeń. Dodajmy przeciążenie metody Search, która przeszukuje wszystkie podkatalogi w poszukiwaniu plików.

Ta metoda może być długotrwałą operacją w katalogu z wieloma podkatalogami. Dodajmy zdarzenie, które zostanie zgłoszone po rozpoczęciu każdego nowego wyszukiwania w katalogu. To zdarzenie umożliwia subskrybentom śledzenie postępu i aktualizowanie użytkownika w miarę postępu. Wszystkie utworzone do tej pory przykłady są publiczne. Utwórzmy to zdarzenie jako zdarzenie wewnętrzne. Oznacza to, że można również ustawić typy argumentów jako wewnętrzne.

Zacznij 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 opracować 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ść zdarzenia z dodawaniem i usuwaniem procedur obsługi. W tym przykładzie nie potrzebujesz 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 wcześniej wyświetlonych definicji zdarzeń pól. Zdarzenie jest tworzone przy użyciu składni podobnej do właściwości . Zwróć uwagę, że programy obsługi mają różne nazwy: add i remove. Te akcesory są wywoływane w celu zasubskrybowania zdarzenia lub wycofania subskrypcji tego zdarzenia. Należy również zadeklarować prywatne pole zapasowe, aby przechowywać zmienną zdarzenia. Ta zmienna jest inicjowana na wartość null.

Następnie dodajmy przeciążenie metody Search, które przechodzi przez podkatalogi i wywołuje oba zdarzenia. Najprostszym sposobem jest użycie domyślnego argumentu w celu określenia, że 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)
        {
            _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;
    }
}

W tym momencie możesz uruchomić aplikację, wywołując funkcję przeciążoną do wyszukiwania wszystkich podkatalogów. Nie ma subskrybentów w nowym zdarzeniu DirectoryChanged, ale użycie idiomu ?.Invoke() zapewnia, że działa poprawnie.

Dodajmy procedurę, aby wypisać 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...");
};

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

Zobacz też

Następnie w najnowszej wersji platformy .NET zobaczysz pewne zmiany w tych wzorcach.