Partilhar via


Padrões de eventos .NET padrão

Anterior

Os eventos .NET geralmente seguem alguns padrões conhecidos. Padronizar esses padrões significa que os desenvolvedores podem aplicar o conhecimento desses padrões padrão, que pode ser aplicado a qualquer programa de evento .NET.

Vamos analisar 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 delegados de eventos

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

void EventRaised(object sender, EventArgs args);

Esta 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. Levantar um evento notifica todos os ouvintes. Em geral, os ouvintes não fornecem valores em resposta a 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 do sender é System.Object, mesmo que tu provavelmente conheças um tipo mais derivado que seria sempre correto. Por convenção, use object.
  • Eventos empacotam mais informações numa única estrutura: O args parâmetro é um tipo derivado de System.EventArgs que inclui toda a informação adicional necessária. (Você verá na próxima seção que essa convenção não é mais aplicada.) 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 seu evento não contém nenhuma informação adicional.

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

O uso de um modelo de evento oferece algumas vantagens de design. Você pode criar vários ouvintes de eventos que executam ações diferentes quando um arquivo procurado é encontrado. Combinar os diferentes ouvintes pode criar algoritmos mais robustos.

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

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

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

Mesmo que esse tipo pareça um tipo pequeno e somente de 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 dos dados são visualizadas por todos os assinantes. A primeira versão é um objeto imutável. Você deve preferir que as propriedades do tipo de argumento do seu evento sejam 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ê verá mais adiante.)

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. Basta usar uma especialização genérica.

Vamos preencher a classe FileSearcher para procurar arquivos que correspondam a um padrão e gerar 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 levantar eventos associados 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;

Parece que está a declarar um campo público, o que pode ser uma má prática de programação orientada a objetos. Você deseja proteger o acesso aos 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 um 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 utilizasses o corpo da lambda, o remove manipulador não funcionaria corretamente. Seria uma instância diferente do delegado, e não faria nada de forma discreta.

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

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

Valores de retorno de assinantes do evento

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

Quando você gera o evento Found, os ouvintes devem ser capazes de interromper o processamento adicional, se esse arquivo for 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 do evento podem usar para comunicar o cancelamento.

Dois padrões diferentes podem ser usados, com base na semântica do contrato de Cancelamento. Em ambos os casos, você adiciona um campo booleano ao EventArguments para o evento de arquivo encontrado.

Um padrão permitiria que qualquer assinante cancelasse a operação. Para esse padrão, o novo campo é inicializado como false. Qualquer assinante pode alterá-lo para true. Após o aumento do evento para todos os assinantes, o componente FileSearcher examina o valor booleano e executa uma ação.

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

Vamos implementar a primeira versão para este exemplo. Você precisa adicionar um campo booleano nomeado CancelRequested ao FileFoundArgs tipo:

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

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

Este novo campo é inicializado automaticamente para false para que você não cancele acidentalmente. A única outra mudança no componente é verificar a bandeira depois de levantar o evento para ver se algum 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 deste padrão é que ele não é uma mudança disruptiva. Nenhum dos assinantes solicitou cancelamento antes, e eles ainda não solicitaram. Nenhum dos códigos de assinante requer atualizações, a menos que eles queiram suportar o novo protocolo de cancelamento.

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

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

Adicionando outra declaração de evento

Vamos adicionar mais um recurso e demonstrar outras expressões idiomáticas para eventos. Vamos adicionar uma sobrecarga ao método Search que atravessa todos os subdiretórios em busca de ficheiros.

Este método pode chegar a ser uma operação demorada em um diretório com muitos subdiretórios. Vamos adicionar um evento que é gerado quando cada nova pesquisa de diretório começa. Esse evento permite que os assinantes acompanhem o progresso e atualizem o usuário quanto ao progresso. Todas as amostras que você criou até agora são públicas. Vamos fazer deste evento um evento interno. Isso significa que você também pode tornar os tipos de argumento internos também.

Você começa criando a nova classe derivada EventArgs para relatar o novo diretório e 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 de evento.

Em seguida, defina o evento. Desta vez, você usa 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;

De muitas maneiras, 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 sintaxe semelhante às propriedades . Observe que os manipuladores têm nomes diferentes: add e remove. Esses acessadores são chamados para se inscrever no evento ou cancelar a inscrição no evento. Observe que você também deve declarar um campo de suporte privado para armazenar a variável de evento. Esta variável é inicializada como null.

Vamos adicionar a sobrecarga do método Search que explora subdiretórios e dispara ambos os 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 ponto, você pode executar o aplicativo chamando a sobrecarga para pesquisar todos os subdiretórios. Não há inscritos no novo evento DirectoryChanged, mas usar o idioma ?.Invoke() garante que ele funcione corretamente.

Vamos adicionar um manipulador para escrever uma linha que mostre 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 .NET. Ao aprender esses padrões e convenções, você está escrevendo C# e .NET idiomáticas rapidamente.

Consulte também

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