Поделиться через


Стандартные шаблоны событий .NET

Предыдущий

События .NET обычно следуют нескольким известным шаблонам. Стандартизация этих шаблонов означает, что разработчики могут применять знания об этих стандартных шаблонах, которые можно применить к любой программе событий .NET.

Давайте рассмотрим эти стандартные шаблоны, чтобы у вас были все знания, необходимые для создания стандартных источников событий, а также подписки и обработки стандартных событий в коде.

Подписи делегатов событий

Стандартной сигнатурой делегата события .NET является:

void EventRaised(object sender, EventArgs args);

Эта стандартная сигнатура предоставляет представление о том, когда используются события:

  • Тип возвращаемого значения — void. Событие может иметь от нуля до многих слушателей. Вызов события уведомляет всех прослушивателей. Как правило, слушатели не предоставляют значения в ответ на события.
  • События указывают отправителя: подпись события включает объект, вызвавшее событие. Это обеспечивает любой прослушиватель механизмом для взаимодействия с отправителем. Тип времени компиляции senderSystem.Object, хотя вы, вероятно, знаете более специфичный тип, который всегда будет правильным. По соглашению используйте object.
  • События с пакетом более полной информации в одной структуре: args параметр является типом, производным от System.EventArgs, который содержит любую дополнительную необходимую информацию. (Вы увидите в следующем разделе , что это соглашение больше не применяется.) Если тип вашего события не требует дополнительных аргументов, все равно необходимо предоставить оба аргумента. Существует специальное значение, EventArgs.Empty, которое следует использовать для обозначения того, что событие не содержит дополнительных сведений.

Создадим класс, который перечисляет соответствующие шаблону файлы в каталоге или любом из его подкаталогов. Этот компонент создает событие для каждого найденного файла, который соответствует шаблону.

Использование модели событий обеспечивает некоторые преимущества разработки. Можно создать несколько прослушивателей событий, которые выполняют разные действия при нахождении искомого файла. Сочетание разных прослушивателей позволяет создавать более надежные алгоритмы.

Ниже приведено начальное объявление аргумента события для поиска запрошенного файла:

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

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

Несмотря на то, что этот тип выглядит как небольшой тип, содержащий только данные, вы должны выполнить соглашение и назначить его ссылочным типом (class). Это означает, что объект аргумента передается по ссылке, и все обновления данных просматриваются всеми подписчиками. Первая версия является неизменяемым объектом. Рекомендуется сделать свойства в типе аргумента события неизменяемыми. Таким образом, один подписчик не может изменить значения, прежде чем другой подписчик увидит их. (Существуют исключения для этой практики, как вы видите позже.)

В следующем шаге нужно создать объявление события в классе FileSearcher. Использование типа System.EventHandler<TEventArgs> означает, что вам не нужно создавать еще одно определение типа. Вы просто используете универсальную специализацию.

Заполним класс FileSearcher для поиска файлов, соответствующих шаблону, и вызова правильного события при обнаружении совпадения.

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

Определение и вызов событий, таких как поле

Самый простой способ добавить событие в класс — объявить это событие как открытое поле, как показано в предыдущем примере.

public event EventHandler<FileFoundArgs>? FileFound;

Это похоже на объявление общедоступного поля, которое, как представляется, является плохой объектно-ориентированной практикой. поскольку необходимо обеспечить защиту доступа к данным с помощью свойств и методов. Хотя этот код может выглядеть как плохая практика, код, созданный компилятором, создает оболочки, чтобы объекты событий могли быть доступны только безопасным способом. Единственными операциями, доступными для события, подобного полю, являются добавление и удаление обработчика.

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

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

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

Для обработчика существует локальная переменная. Если вы использовали тело лямбда-функции, обработчик remove не работал бы корректно. Будет существовать другой экземпляр делегата, не выполняющий никаких действий.

Код за пределами класса не может вызвать событие и не может выполнять другие операции.

Начиная с C# 14 события можно объявить как частичные элементы. Частичное объявление события должно включать определяющее объявление и объявление для реализации. Определяющее объявление должно использовать синтаксис событий, аналогичный синтаксису полей. Объявление реализации должно объявлять обработчики add и remove.

Возврат значений от подписчиков событий

Простая версия работает нормально. Давайте добавим еще одну возможность — отмену.

При вызове события Found прослушиватели должны прекратить дальнейшую обработку, если этот файл является последним искомым.

Обработчики событий не возвращают значение, поэтому необходимо сообщить об этом другим способом. Стандартный шаблон событий EventArgs использует объект для включения полей, которые подписчики событий могут использовать для передачи информации об отмене.

Можно использовать два разных шаблона на основе семантики договора об отмене. В обоих случаях вы добавляете логическое поле в EventArguments для события обнаруженного файла.

Один шаблон позволяет любому одному подписчику отменить операцию. Для этого шаблона новое поле инициализируется значением false. Любой подписчик может изменить это на true. После вызова события для всех подписчиков компонент FileSearcher проверяет логическое значение и принимает меры.

Второй шаблон отменит операцию только в том случае, если все подписчики хотели отменить операцию. В этом шаблоне новое поле инициализируется для указания того, что операцию следует отменить, и любой подписчик может изменить его, чтобы указать, что следует продолжить операцию. После того как все подписчики обработали вызванное событие, компонент FileSearcher проверяет логическое значение и принимает меры. В этом шаблоне есть еще один дополнительный шаг: компонент должен знать, ответил ли любой подписчик на событие. Если подписчики отсутствуют, поле неверно сообщит об отмене.

Реализуем первую версию для этого примера. Добавьте логическое поле с именем CancelRequested в тип FileFoundArgs:

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

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

Это новое поле автоматически инициализируется в false, чтобы не отменить по ошибке. Единственное другое изменение компонента заключается в проверке флага после вызова события, чтобы узнать, запрашивает ли любой из подписчиков отмену:

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

Одно из преимуществ этого шаблона заключается в том, что это не является критическим изменением. Никто из подписчиков не запрашивал отмену раньше, и они до сих пор этого не сделали. Ни один из кодов подписчика не требует обновлений, если они не хотят поддерживать новый протокол отмены.

Изменим подписчика, чтобы он инициировал отмену, как только обнаружит первый исполняемый файл.

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

Добавление другой декларации события

Добавим еще одну функцию и продемонстрируем языковые идиомы для описания событий. Добавим перегрузку метода Search, который проходит через все подкаталоги в поиске файлов.

Этот метод может быть длительной операцией в каталоге с множеством подкаталогов. Добавим событие, которое вызывается в начале каждого нового поиска в каталоге. Это событие позволяет подписчикам отслеживать ход выполнения и информировать пользователя о ходе выполнения. Все созданные вами примеры являются общедоступными. Давайте сделаем это событие внутренним событием. Это означает, что также можно сделать типы аргументов внутренними.

Начните с создания нового производного класса EventArgs для создания отчетов о новом каталоге и ходе выполнения.

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

Опять же, вы можете следовать рекомендациям по созданию неизменяемого ссылочного типа для аргументов событий.

Теперь определим событие. На этот раз используется другой синтаксис. Помимо использования синтаксиса поля, вы можете явно создать свойство события с помощью обработчиков добавления и удаления. В этом примере не требуется дополнительный код в этих обработчиках, но в этом примере показано, как создать их.

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

Во многих отношениях код, который вы пишете здесь, отражает код, который компилятор создает для определений событий поля, которые вы видели ранее. Событие создается с помощью синтаксиса, аналогичного свойствам . Обратите внимание, что обработчики имеют разные имена: add и remove. Эти акцессоры используются для подписки на событие или отмены подписки на событие. Учтите, что вы также должны объявить закрытое резервное поле для хранения переменной событий. Эта переменная инициализирована в null.

Теперь добавим перегрузку метода Search, который обходит подкаталоги и вызывает оба события. Самый простой способ — использовать аргумент по умолчанию, чтобы указать, что вы хотите искать все каталоги:

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

На этом этапе можно запустить приложение, вызывающее перегрузку для поиска всех подкаталогов. Нет подписчиков на новом событии DirectoryChanged, но использование идиомы ?.Invoke() гарантирует правильную работу.

Добавим обработчик для написания строки, показывающей ход выполнения в окне консоли.

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

Вы видели шаблоны, которые используются во всей экосистеме .NET. Изучая эти шаблоны и соглашения, вы быстро пишете идиоматические C# и .NET.

См. также

Далее вы увидите некоторые изменения в этих шаблонах в последнем выпуске .NET.