8. Eventos de usuário
No capítulo anterior, abordamos o conceito de eventos relacionados a componentes de formulário. Veremos agora como criar eventos em nossas próprias classes.
8.1. Objetos delegados predefinidos
O conceito de objeto delegate foi abordado no capítulo anterior, mas, na ocasião, foi tratado superficialmente. Ao analisarmos como os manipuladores de eventos dos componentes de um formulário eram declarados, encontramos um código semelhante ao seguinte:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
onde buttonAfficher era um componente do tipo [Button]. Essa classe possui um campo Click definido da seguinte forma:
![]() |
- [1]: a classe [Button]
- [2]: seus eventos
- [3,4]: o evento Click
- [5]: a declaração do evento [Control.Click] [4].
- EventHandler é um protótipo (um modelo) de método chamado delegado.
- event é uma palavra-chave que restringe as funcionalidades do delegate e do EventHandler: um objeto delegate possui funcionalidades mais abrangentes do que um objeto event.
O delegate EventHandler é definido da seguinte forma:
![]() |
O delegate EventHandler designa um modelo de método:
- cujo primeiro parâmetro é do tipo Object
- cujo segundo parâmetro é do tipo EventArgs
- que não retorna nenhum resultado
Um método correspondente ao modelo definido por EventHandler poderia ser o seguinte:
private void buttonAfficher_Click(object sender, EventArgs e);
Para criar um objeto do tipo EventHandler, proceda da seguinte forma:
EventHandler evtHandler=new EventHandler(méthode correspondant au prototype défini par le type EventHandler);
Assim, será possível escrever:
Uma variável do tipo delegate é, na verdade, uma lista de referências a métodos correspondentes ao modelo do delegate. Para adicionar um novo método M à variável evtHandler acima, utiliza-se a sintaxe:
A notação += pode ser usada mesmo que evtHandler seja uma lista vazia.
A instrução:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
adiciona um método do tipo EventHandler à lista de métodos do evento buttonAfficher.Click. Quando ocorre o evento Click no componente buttonAfficher, o VB executa a instrução:
onde:
- source é o componente do tipo object que originou o evento
- evt, do tipo EventArgs, e não contém informações
Todos os métodos de assinatura void M(object,EventArgs) que foram associados ao evento Click por:
this.buttonAfficher.Click += new System.EventHandler(M);
serão chamados com os parâmetros (source, evt) transmitidos por VB.
8.2. Definir objetos delegados
A instrução
public delegate int Opération(int n1, int n2);
define um tipo chamado Opération, que representa um protótipo de função que aceita dois inteiros e retorna um inteiro. É a palavra-chave delegate que faz de Opération uma definição de protótipo de função.
Uma variável op do tipo Opération terá a função de armazenar uma lista de funções correspondentes ao protótipo Opération:
O registro de um método fi na variável op é feito por op=new Operação(fi) ou, mais simplesmente, por op=fi. Para adicionar um método fj à lista de funções já registradas, escreve-se op+= fj. Para remover um método fk já registrado, escreve-se op-=fk. Se, em nosso exemplo, escrevermos n=op(n1,n2), todos os métodos registrados na variável op serão executados com os parâmetros n1 e n2. O resultado n recuperado será o do último método executado. Não é possível obter os resultados produzidos por todos os métodos. Por esse motivo, se registrarmos uma lista de métodos em uma função delegada, estes geralmente retornam um resultado do tipo void.
Consideremos o seguinte exemplo:
using System;
namespace Chap6 {
class Class1 {
// definição de um protótipo de função
// aceita dois inteiros como parâmetros e retorna um inteiro
public delegate int Opération(int n1, int n2);
// dois métodos de instância correspondentes ao protótipo
public int Ajouter(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}//adicionar
public int Soustraire(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}//subtrair
// um método estático correspondente ao protótipo
public static int Augmenter(int n1, int n2) {
Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
return n1 + 2 * n2;
}//aumentar
static void Main(string[] args) {
// define-se um objeto do tipo operação para registrar funções nele
// registra-se a função estática aumentar
Opération op = Augmenter;
// executa-se o delegado
int n = op(4, 7);
Console.WriteLine("n=" + n);
// criação de um objeto c1 do tipo class1
Class1 c1 = new Class1();
// registra-se no delegado o método “adicionar” de c1
op = c1.Ajouter;
// execução do objeto delegado
n = op(2, 3);
Console.WriteLine("n=" + n);
// registra-se no delegado o método “subtrair” de c1
op = c1.Soustraire;
n = op(2, 3);
Console.WriteLine("n=" + n);
//Registro de duas funções no delegado
op = c1.Ajouter;
op += c1.Soustraire;
// execução do objeto delegado
op(0, 0);
// remoção de uma função do delegado
op -= c1.Soustraire;
// executa-se o delegado
op(1, 1);
}
}
}
- linha 3: define uma classe Class1.
- linha 6: definição de delegate Opération: um protótipo de métodos que aceita dois parâmetros do tipo int e retorna um resultado do tipo int
- linhas 9-12: o método de instância Ajouter possui a assinatura do delegado Opération.
- linhas 14-17: o método de instância Soustraire possui a assinatura do delegado Opération.
- linhas 20-23: o método de classe Augmenter possui a assinatura do delegado Opération.
- linha 25: o método Main foi executado
- linha 20: a variável op é do tipo delegado Opération. Ela conterá uma lista de métodos com a assinatura do tipo delegado Opération. É atribuída a ela uma primeira referência de método, a do método estático Class1.Augmenter.
- linha 31: o delegate op é executado: todos os métodos referenciados pelo op serão executados. Eles serão executados com os parâmetros passados para o delegate e o op.. Aqui, apenas o método estático Class1.Augmenter será executado.
- linha 35: é criada uma instância c1 da classe Class1.
- linha 37: o método de instância c1.Ajouter é atribuído ao delegado op. Augmenter era um método estático, Ajouter é um método de instância. O objetivo era mostrar que isso não fazia diferença.
- linha 39: o delegate op é executado: o método Ajouter será executado com os parâmetros passados para o delegate op.
- linha 42: fazemos o mesmo com o método de instância Soustraire.
- linhas 46-47: os métodos Ajouter e Soustraire são inseridos no delegate op.
- linha 49: o delegate op é executado: os dois métodos Ajouter e Soustraire serão executados com os parâmetros passados para o delegate e op.
- linha 51: o método Soustraire é removido do delegate op.
- linha 53: o delegate op é executado: o método restante, Ajouter, será executado.
Os resultados da execução são os seguintes:
8.3. Delegados ou interfaces?
Os conceitos de delegates e de interfaces podem parecer bastante semelhantes, e é possível questionar quais são exatamente as diferenças entre esses dois conceitos. Consideremos o exemplo a seguir, semelhante a um exemplo já estudado:
using System;
namespace Chap6 {
class Program1 {
// definição de um protótipo de função
// aceita dois inteiros como parâmetros e retorna um inteiro
public delegate int Opération(int n1, int n2);
// dois métodos de instância correspondentes ao protótipo
public static int Ajouter(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}//adicionar
public static int Soustraire(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}//subtrair
// Execução de um delegado
public static int Execute(Opération op, int n1, int n2){
return op(n1, n2);
}
static void Main(string[] args) {
// execução do delegado Adicionar
Console.WriteLine(Execute(Ajouter, 2, 3));
// execução do delegado Subtrair
Console.WriteLine(Execute(Soustraire, 2, 3));
// execução de um delegado multicast
Opération op = Ajouter;
op += Soustraire;
Console.WriteLine(Execute(op, 2, 3));
// remoção de uma função do delegado
op -= Soustraire;
// executa-se o delegado
Console.WriteLine(Execute(op, 2, 3));
}
}
}
Na linha 20, o método Execute espera uma referência a um objeto do tipo delegado Opération, definido na linha 6. Isso permite passar para o método Execute diferentes métodos (linhas 26, 28, 32 e 36). Essa propriedade de polimorfismo também pode ser obtida com uma interface:
using System;
namespace Chap6 {
// interface IOperation
public interface IOperation {
int operation(int n1, int n2);
}
// classe Adicionar
public class Ajouter : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}
}
// classe Subtrair
public class Soustraire : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}
}
// classe de teste
public static class Program2 {
// Execução do único método da interface IOperation
public static int Execute(IOperation op, int n1, int n2) {
return op.operation(n1, n2);
}
public static void Main() {
// execução do delegado Adicionar
Console.WriteLine(Execute(new Ajouter(), 2, 3));
// Execução do delegado Subtrair
Console.WriteLine(Execute(new Soustraire(), 2, 3));
}
}
}
- linhas 6-8: a interface [IOperation] define um método operation.
- linhas 11-16 e 19-24: as classes [Ajouter] e [Soustraire] implementam a interface [IOperation].
- linhas 29-31: o método Execute, cujo primeiro parâmetro é do tipo da interface IOperation. O método Execute receberá sucessivamente como primeiro parâmetro uma instância da classe Ajouter e, em seguida, uma instância da classe Soustraire.
Vemos aqui claramente o aspecto polimórfico que o parâmetro do tipo delegate apresentava no exemplo anterior. Os dois exemplos mostram, ao mesmo tempo, as diferenças entre esses dois conceitos.
Os tipos delegate e interface são intercambiáveis
- se a interface tiver apenas um método. De fato, o tipo delegate é um invólucro para um único método, enquanto a interface pode definir vários métodos.
- se o aspecto multicast do delegate não for utilizado. De fato, esse conceito de multicast não existe na interface.
Se essas duas condições forem verificadas, então temos a opção de escolher entre as duas assinaturas a seguir para o método Execute:
int Execute(IOperation op, int n1, int n2)
int Execute(Opération op, int n1, int n2)
A segunda, que utiliza o delegate, pode ser mais flexível de uso. De fato, na primeira assinatura, o primeiro parâmetro do método deve implementar a interface IOperation. Isso obriga a criar uma classe para definir o método que será passado como primeiro parâmetro ao método Execute. Na segunda assinatura, qualquer método existente com a assinatura correta serve. Não há necessidade de nenhuma construção adicional.
8.4. Gerenciamento de eventos
Os objetos delegate podem ser usados para definir eventos. Uma classe C1 pode definir um evento evt da seguinte maneira:
- um tipo delegate é definido dentro ou fora da classe C1:
- a classe C1 define um campo do tipo delegate Evt:
- quando uma instância c1 da classe C1 quiser sinalizar um evento, ela executará seu delegado Evt1, passando-lhe os parâmetros definidos pelo delegado Evt. Todos os métodos registrados no delegate Evt1 serão então executados com esses parâmetros. Pode-se dizer que eles foram notificados sobre o evento Evt1.
- Se um objeto c2, que utiliza um objeto c1, quiser ser notificado sobre a ocorrência do evento Evt1 no objeto c1, ele registrará um de seus métodos c2.M no objeto delegado c1.Evt1 do objeto c1.. Assim, seu método c2.M será executado sempre que oevento Evt1 ocorrer no objeto c1. Ele também poderá cancelar a inscrição quando não quiser mais ser notificado sobre o evento.
- Como o objeto delegado c1.Evt1 pode registrar vários métodos, diferentes objetos ci poderão se registrar junto ao delegado c1.Evt1 para serem notificados sobre o evento Evt1 no c1.
Nesse cenário, temos:
- uma classe que sinaliza um evento
- classes que são notificadas sobre esse evento. Diz-se que elas se inscrevem no evento ou se assinam o evento.
- um tipo delegate que define a assinatura dos métodos que serão notificados sobre o evento
O framework .NET define:
- uma assinatura padrão do delegate de um evento
- source: o objeto que relatou o evento
- evtInfo: um objeto do tipo EventArgs ou derivado que fornece informações sobre o evento
- o nome do delegate deve terminar com EventHandler
- uma forma padrão de declarar um evento do tipo MyEventHandler em uma classe:
O campo Evt1 é do tipo delegate. A palavra-chave event serve para restringir as operações que podem ser realizadas sobre ele:
- de fora da classe C1, apenas as operações += e -= são possíveis. Isso impede a exclusão (por exemplo, por erro do desenvolvedor) dos métodos inscritos no evento. É possível simplesmente inscrever-se (+=) ou cancelar a inscrição (-=) no evento.
- apenas uma instância do tipo C1 pode executar a chamada Evt1(source,evtInfo), que aciona a execução dos métodos inscritos no evento Evt1.
O framework .NET fornece um método genérico que atende à assinatura do delegate de um evento:
public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
- o delegate EventHandler utiliza o tipo genérico TEventArgs, que é o tipo de seu segundo parâmetro
- o tipo TEventArgs deve derivar do tipo EventsArgs (onde TEventArgs: EventArgs)
Com esse delegate genérico, a declaração de um evento X na classe C seguirá o esquema recomendado a seguir:
- definir um tipo XEventArgs derivado de EventArgs para encapsular as informações sobre o evento X
- definir, na classe C, um evento do tipo EventHandler<XEventArgs>.
- definir na classe C um método protegido
destinado a “publicar” o evento X para os assinantes.
Consideremos o seguinte exemplo:
- uma classe Emetteur encapsula uma temperatura. Essa temperatura é monitorada. Quando essa temperatura ultrapassa um determinado limite, um evento deve ser disparado. Chamaremos esse evento de TemperatureTropHaute. As informações sobre esse evento serão encapsuladas em um tipo TemperatureTropHauteEventArgs.
- Uma classe Souscripteur assina o evento anterior. Quando é notificada sobre o evento, ela exibe uma mensagem no console.
- Um programa de console cria um emissor e dois assinantes. Ele insere as temperaturas pelo teclado e as registra em uma instância Emetteur. Se a temperatura estiver muito alta, a instância Emetteur publica o evento TemperatureTropHaute.
Para seguir a prática recomendada de gerenciamento de eventos, definimos primeiro o tipo TemperatureTropHauteEventArgs para encapsular as informações sobre o evento:
using System;
namespace Chap6 {
public class TemperatureTropHauteEventArgs:EventArgs {
// temperatura no momento do evento
public decimal Temperature { get; set; }
// fabricantes
public TemperatureTropHauteEventArgs() {
}
public TemperatureTropHauteEventArgs(decimal temperature) {
Temperature = temperature;
}
}
}
- linha 6: a informação encapsulada pela classe TemperatureTropHauteEventArgs é a temperatura que provocou o evento TemperatureTropHaute.
A classe Emetteur é a seguinte:
using System;
namespace Chap6 {
public class Emetteur {
static decimal SEUIL = 19;
// temperatura observada
private decimal temperature;
// nome da fonte
public string Nom { get; set; }
// evento relatado
public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;
// leitura/gravação da temperatura
public decimal Temperature {
get {
return temperature;
}
set {
temperature = value;
if (temperature > SEUIL) {
// o evento é notificado aos assinantes
OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
}
}
}
// notificação de um evento
protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
// transmissão do evento TemperatureTropHaute aos assinantes
TemperatureTropHaute(this, evt);
}
}
}
- linha 5: o limite de temperatura acima do qual o evento TemperatureTropHaute será publicado.
- linha 10: o emissor possui um nome para ser identificado
- linha 12: o evento TemperatureTropHaute.
- linhas 15-26: o método get, que retorna a temperatura, e o método set, que a registra. É o método set que faz com que o evento TemperatureTropHaute seja publicado caso a temperatura a ser registrada ultrapasse o limite da linha 5. Ele faz com que o evento seja publicado pelo método OnTemperatureTropHauteHandler da linha 29, passando como parâmetro um objeto TemperatureTropHauteEventArgs no qual foi registrada a temperatura que ultrapassou o limite.
- linhas 29-32: o evento TemperatureTropHaute é publicado tendo como primeiro parâmetro o próprio emissor e, como segundo parâmetro, o objeto TemperatureTropHauteEventArgs recebido como parâmetro.
A classe Souscripteur, que se inscreverá no evento TemperatureTropHaute, é a seguinte:
using System;
namespace Chap6 {
public class Souscripteur {
// nome
public string Nom { get; set; }
// gerente do evento TemperatureTropHaute
public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
// exibição no console do operador
Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
}
}
}
- linha 6: cada assinante é identificado por um nome.
- linhas 9-12: o método que será associado ao evento TemperatureTropHaute. Ela possui a assinatura do tipo delegate EventHandler<TEventArgs>, que um gerenciador de eventos deve ter. O método exibe no console: o nome do assinante que está exibindo a mensagem, o nome do emissor que sinalizou o evento e a temperatura que o acionou.
- A assinatura do evento TemperatureTropHaute de um objeto Emetteur não é feita na classe Souscripteur. Ela será feita por uma classe externa.
O programa [Program.cs] vincula todos esses elementos entre si:
using System;
namespace Chap6 {
class Program {
static void Main(string[] args) {
// criação de um emissor de eventos
Emetteur e1 = new Emetteur() { Nom = "e" };
// criação de uma tabela com 2 assinantes
Souscripteur[] souscripteurs = new Souscripteur[2];
for (int i = 0; i < souscripteurs.Length; i++) {
// criação de assinante
souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
// inscreve-o no evento TemperatureTropHaute de e1
e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
}
// as temperaturas são inseridas pelo teclado
decimal temperature;
Console.Write("Température (rien pour arrêter) : ");
string saisie = Console.ReadLine().Trim();
// desde que a linha inserida não esteja vazia
while (saisie != "") {
// a entrada é um número decimal?
if (decimal.TryParse(saisie, out temperature)) {
// temperatura correta — é registrada
e1.Temperature = temperature;
} else {
// é sinalizado o erro
Console.WriteLine("Température incorrecte");
}
// nova entrada
Console.Write("Température (rien pour arrêter) : ");
saisie = Console.ReadLine().Trim();
}//enquanto
}
}
}
- linha 6: criação do emissor
- linhas 8-14: criação de dois assinantes que são inscritos no evento TemperatureTropHaute do emissor.
- linhas 20-32: loop para inserção das temperaturas pelo teclado
- linha 24: se a temperatura inserida estiver correta, ela é transmitida ao objeto Emetteur e1, que acionará o evento TemperatureTropHaute caso a temperatura seja superior a 19 °C.
Os resultados da execução são os seguintes:

