Padrões de eventos .NET padrão
Os eventos .NET geralmente seguem alguns padrões conhecidos. A padronização desses padrões significa que os desenvolvedores podem aproveitar o conhecimento desses padrões padrão, que podem ser aplicados a qualquer programa de eventos .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);
O tipo de retorno é nulo. Os eventos são baseados em delegados e são delegados multicast. Isso suporta vários assinantes para qualquer fonte de evento. O valor de retorno único de um método não é dimensionado para vários assinantes de eventos. Qual valor de retorno a fonte do evento vê depois de gerar um evento? Mais adiante neste artigo, você verá como criar protocolos de evento que oferecem suporte a assinantes de eventos que relatam informações para a fonte do evento.
A lista de argumentos contém dois argumentos: o remetente e os argumentos de evento. O tipo de tempo de compilação é sender
System.Object
, mesmo que você provavelmente conheça um tipo mais derivado que sempre estaria correto. Por convenção, use object
.
O segundo argumento tem sido tipicamente um tipo derivado de System.EventArgs
. (Você verá na próxima seção que essa convenção não é mais aplicada.) Se o tipo de evento não precisar de argumentos adicionais, você ainda fornecerá os dois 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 será passado por referência e quaisquer atualizações dos dados serão visualizadas por todos os assinantes. A primeira versão é um objeto imutável. Você deve preferir 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 isso, como você verá abaixo.)
Em seguida, precisamos criar a declaração de evento na classe FileSearcher. Aproveitar o EventHandler<T>
tipo significa que você não precisa criar outra 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))
{
RaiseFileFound(file);
}
}
private void RaiseFileFound(string file) =>
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 que está declarando um campo público, o que parece ser uma má prática orientada a objetos. Você deseja proteger o acesso aos dados por meio de propriedades ou métodos. Embora isso 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 add handler:
var fileLister = new FileSearcher();
int filesFound = 0;
EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
Console.WriteLine(eventArgs.FoundFile);
filesFound++;
};
fileLister.FileFound += onFileFound;
e remover manipulador:
fileLister.FileFound -= onFileFound;
Observe que há uma variável local para o manipulador. Se você usasse o corpo do lambda, a remoção não funcionaria corretamente. Seria uma instância diferente do delegado, e silenciosamente não fazer nada.
O código fora da classe não pode gerar o evento, nem pode executar quaisquer outras operações.
Valores de retorno de assinantes do evento
Sua versão simples está funcionando bem. Vamos adicionar outro recurso: Cancelamento.
Quando você gera o evento encontrado, 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 padrão usa o EventArgs
objeto para incluir campos que os assinantes do evento podem usar para comunicar cancelar.
Dois padrões diferentes podem ser usados, com base na semântica do contrato Cancelar. Em ambos os casos, você adicionará 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
. Depois que todos os assinantes tiverem visto o evento gerado, 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 viram o evento gerado, o componente FileSearcher examina o booleano e toma uma ação. Há uma etapa extra neste padrão: o componente precisa saber se algum assinante viu o 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;
}
Esse novo campo é inicializado automaticamente como false
, o valor padrão de um Boolean
campo, 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))
{
FileFoundArgs args = RaiseFileFound(file);
if (args.CancelRequested)
{
break;
}
}
}
private FileFoundArgs RaiseFileFound(string file)
{
var args = new FileFoundArgs(file);
FileFound?.Invoke(this, args);
return args;
}
Uma vantagem desse padrão é que ele não é uma mudança de rutura. Nenhum dos assinantes solicitou cancelamento antes, e eles ainda não estão. Nenhum código de assinante precisa ser atualizado, a menos que eles queiram suportar o novo protocolo de cancelamento. É muito frouxamente acoplado.
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 do Search
método que atravessa todos os subdiretórios em busca de arquivos.
Isso 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. Isso 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 um evento interno. Isso significa que você também pode tornar os tipos usados para os argumentos internos também.
Você começará criando a nova classe derivada de 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 de 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, com manipuladores de adição e remoção. Neste exemplo, você não precisará 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 uma sintaxe muito semelhante à usada para propriedades. Observe que os manipuladores têm nomes diferentes: add
e remove
. Estes são chamados a inscrever-se no evento ou a cancelar a subscrição do mesmo. Observe que você também deve declarar um campo de suporte privado para armazenar a variável de evento. Ele é inicializado como null.
Em seguida, vamos adicionar a Search
sobrecarga do método que atravessa subdiretórios e gera ambos os eventos. A maneira mais fácil de fazer isso é 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)
{
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;
}
Neste ponto, você pode executar o aplicativo chamando a sobrecarga para pesquisar todos os subdiretórios. Não há inscritos no novo DirectoryChanged
evento, mas usar o ?.Invoke()
idioma garante que isso 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ê estará escrevendo C# e .NET idiomáticos rapidamente.
Consulte também
Em seguida, você verá algumas alterações nesses padrões na versão mais recente do .NET.