Compartilhar via


Padrões de evento .NET padrão

Anterior

Os eventos do .NET geralmente seguem alguns padrões conhecidos. Padronizar esses padrões significa que os desenvolvedores podem aplicar conhecimento desses padrões, que podem ser aplicados a qualquer programa de eventos do .NET.

Vamos examinar esses padrões padrão para que você tenha todo o conhecimento necessário para criar fontes de eventos padrão e assinar e processar eventos padrão em seu código.

Assinaturas de delegado de evento

A assinatura padrão de um delegado de evento do .NET é:

void EventRaised(object sender, EventArgs args);

Essa assinatura padrão fornece informações sobre quando os eventos são usados:

  • O tipo de retorno é nulo. Os eventos podem ter de zero a muitos ouvintes. A geração de um evento notifica todos os ouvintes. Em geral, os ouvintes não fornecem valores em resposta aos eventos.
  • Eventos indicam o remetente: a assinatura do evento inclui o objeto que gerou o evento. Isso fornece a qualquer ouvinte um mecanismo para se comunicar com o remetente. O tipo de tempo de compilação de sender é System.Object, mas é provável que você conheça um tipo mais derivado que sempre estaria correto. Por convenção, use object.
  • Os eventos empacotam mais informações em uma única estrutura: o args parâmetro é um tipo derivado System.EventArgs que inclui quaisquer informações adicionais necessárias. (Você verá na próxima seção que essa convenção não é mais imposta.) Se o tipo de evento não precisar de mais argumentos, você ainda deverá fornecer ambos os argumentos. Há um valor especial, EventArgs.Empty que você deve usar para indicar que o evento não contém informações adicionais.

Vamos criar uma classe que lista os arquivos em um diretório ou em qualquer um de seus subdiretórios, que seguem um padrão. Esse componente aciona um evento para cada arquivo encontrado que corresponde ao padrão.

O uso de um modelo de evento fornece algumas vantagens de design. Você pode criar vários ouvintes de eventos que realizam ações diferentes quando um arquivo procurado é encontrado. A combinação de diferentes ouvintes pode criar algoritmos mais robustos.

Aqui está a declaração de argumento de evento inicial para localizar um arquivo procurado:

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

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

Embora esse tipo se pareça com um tipo pequeno de somente dados, você deve seguir a convenção e torná-lo um tipo de referência (class). Isso significa que o objeto de argumento é passado por referência e todas as atualizações nos dados são visíveis para todos os assinantes. A primeira versão é um objeto imutável. É preferível tornar as propriedades em seu tipo de argumento de evento imutáveis. Dessa forma, um assinante não pode alterar os valores antes que outro assinante os veja. (Há exceções a essa prática, como você vê mais tarde.)

Em seguida, precisamos criar a declaração de evento na classe FileSearcher. Usar o tipo System.EventHandler<TEventArgs> significa que você não precisa criar mais uma definição de tipo. Você apenas usa uma especialização genérica.

Vamos preencher a classe FileSearcher para pesquisar arquivos que correspondam a um padrão e acionar o evento correto quando uma correspondência for descoberta.

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

Definir e gerar eventos semelhantes a campos

A maneira mais simples de adicionar um evento à sua classe é declarar esse evento como um campo público, como no exemplo anterior:

public event EventHandler<FileFoundArgs>? FileFound;

Isso parece estar declarando um campo público, o que parece ser uma prática orientada a objetos ruim. Você deseja proteger o acesso a dados por meio de propriedades ou métodos. Embora esse código possa parecer uma prática incorreta, o código gerado pelo compilador cria wrappers para que os objetos de evento só possam ser acessados de maneiras seguras. As únicas operações disponíveis em um evento semelhante a campo são adicionar e remover manipulador:

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

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

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

Há uma variável local para o manipulador. Se você tiver usado o corpo de lambda, o manipulador remove não funcionará corretamente. Ela seria uma instância diferente do delegado e silenciosamente não faria nada.

O código fora da classe não pode gerar o evento, nem pode executar outras operações.

A partir do C# 14, os eventos podem ser declarados como membros parciais. Uma declaração de evento parcial deve incluir uma declaração de definição e uma declaração de implementação. A declaração de definição deve usar a sintaxe de evento semelhante a um campo. A declaração de implementação deve declarar os manipuladores add e remove.

Valor retornados de assinantes de evento

Sua versão simples está funcionando bem. Vamos adicionar outro recurso: cancelamento.

Quando você aciona o evento Encontrado, os ouvintes devem conseguir interromper o processamento posterior, caso este arquivo seja o último procurado.

Os manipuladores de eventos não retornam um valor, portanto, você precisa comunicar isso de outra maneira. O padrão de evento usa o objeto EventArgs para incluir campos que os assinantes de evento podem usar para comunicar o cancelamento.

Há dois padrões diferentes que podem ser usados, com base na semântica do contrato de cancelamento. Em ambos os casos, você adiciona um campo booliano aos EventArguments para o evento de arquivo encontrado.

Um padrão permitiria a qualquer assinante cancelar a operação. Para esse padrão, o novo campo é inicializado para false. Qualquer assinante pode alterá-lo para true. Após disparar o evento para todos os assinantes, o componente FileSearcher examina o valor booleano e toma medidas.

O segundo padrão apenas cancelaria a operação se todos os assinantes quisessem que a operação fosse cancelada. Nesse padrão, o novo campo é inicializado para indicar que a operação deve ser cancelada e qualquer assinante poderia alterá-lo para indicar que a operação deve continuar. Depois que todos os assinantes processam o evento gerado, o componente FileSearcher examina o booliano e toma medidas. Há uma etapa extra nesse padrão: o componente precisa saber se algum assinante respondeu ao evento. Se não houver nenhum assinante, o campo indicaria incorretamente um cancelamento.

Vamos implementar a primeira versão deste exemplo. Você precisa adicionar um campo booliano chamado CancelRequested ao tipo FileFoundArgs:

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

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

Esse novo campo é inicializado automaticamente para false para que você não cancele acidentalmente. A única alteração adicional no componente é verificar o sinalizador depois de acionar o evento, para ver se qualquer um dos assinantes solicitou um cancelamento:

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

Uma vantagem desse padrão é que ele não é uma alteração significativa. Nenhum dos assinantes solicitou cancelamento antes, e ainda não solicitaram. Nenhum código do assinante requer atualizações, a menos que ele deseje dar suporte ao novo protocolo de cancelamento.

Vamos atualizar o assinante para que ele solicite um cancelamento, depois de encontrar o primeiro executável:

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

Adicionar outra declaração de evento

Vamos adicionar mais um recurso e demonstrar outras expressões de linguagem para eventos. Vamos adicionar uma sobrecarga do método Search que percorre todas os subdiretórios pesquisando arquivos.

Esse método pode ser uma operação longa em um diretório com muitos subdiretórios. Vamos adicionar um evento que é acionado no início de cada nova pesquisa de diretório. Esse evento permite que os assinantes acompanhem o progresso e atualizem o usuário quanto ao progresso. Todos os exemplos que você criou até agora são públicos. Vamos tornar esse evento um evento interno. Isso significa que você também pode tornar os tipos de argumentos internos.

Você começa criando a nova classe derivada eventArgs para relatar o novo diretório e o progresso.

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

Novamente, você pode seguir as recomendações para criar um tipo de referência imutável para os argumentos do evento.

Em seguida, defina o evento. Desta vez, você usará uma sintaxe diferente. Além de usar a sintaxe de campo, você pode criar explicitamente a propriedade de evento com manipuladores add e remove. Neste exemplo, você não precisa de código extra nesses manipuladores, mas isso mostra como você os criaria.

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

Em muitos aspectos, o código que você escreve aqui espelha o código que o compilador gera para as definições de evento de campo que você viu anteriormente. Você cria o evento usando uma sintaxe semelhante à das propriedades . Observe que os manipuladores têm nomes diferentes: add e remove. Esses acessadores são chamados para assinar o evento ou cancelar a assinatura do evento. Observe que você também deve declarar um campo de suporte particular para armazenar a variável de evento. Essa variável é inicializada como nula.

Em seguida, vamos adicionar a sobrecarga do método Search que percorre os subdiretórios e aciona os dois eventos. A maneira mais fácil é usar um argumento padrão para especificar que você deseja pesquisar todos os diretórios:

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

Neste momento, você pode executar o aplicativo, chamando a sobrecarga para pesquisar todos os subdiretórios. Não há assinantes no novo evento DirectoryChanged, mas usar o idioma ?.Invoke() garante que ele funcione corretamente.

Vamos adicionar um manipulador para escrever uma linha que mostra o progresso na janela do console.

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

Você viu padrões que são seguidos em todo o ecossistema do .NET. Ao aprender esses padrões e convenções, você está escrevendo C# e .NET de forma idiomática rapidamente.

Confira também

Em seguida, você verá algumas alterações nesses padrões na versão mais recente do .NET.