Skip to content

8. Eventos do utilizador

No capítulo anterior, abordámos o conceito de eventos associados a componentes de formulário. Vamos agora ver como criar eventos nas nossas próprias classes.

8.1. Objetos delegate predefinidos

O conceito de objeto delegate foi abordado no capítulo anterior, mas na altura foi apenas mencionado de forma superficial. Quando analisámos como os gestores de eventos dos componentes de um formulário eram declarados, deparámo-nos com código semelhante ao seguinte:


            this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

onde buttonAfficher era um componente do tipo [Button]. Esta classe tem um campo Click definido da seguinte forma:

  • [1]: a classe [Button]
  • [2]: os 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 denominado delegado.
    • event é uma palavra-chave que restringe as funcionalidades do delegate 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 devolve qualquer 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, deve-se proceder da seguinte forma:

EventHandler evtHandler=new EventHandler(méthode correspondant au prototype  défini par le type EventHandler);

Assim, poderá escrever-se:

EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

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:

evtHandler+=new EvtHandler(M);

A notação += pode ser utilizada 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:

            buttonAfficher.Click(source, evt);

onde:

  • source é o componente do tipo object que originou o evento
  • evt, de 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 denominado Opération, que representa um protótipo de função que aceita dois inteiros e devolve um inteiro. É a palavra-chave delegate que faz com que Opération seja uma definição de protótipo de função.

Uma variável op do tipo Opération terá como função registar uma lista de funções correspondentes ao protótipo Opération:

int f1(int,int)
int f2(int,int)
...
int fn(int,int)

O registo de um método fi na variável op é feito através de op=new Operação(fi) ou, mais simplesmente, por op=fi. Para adicionar um método fj à lista de funções já registadas, escreve-se op+= fj. Para remover um método fk já registado, escreve-se op-=fk. Se, no nosso exemplo, escrevermos n=op(n1,n2), o conjunto de métodos registados na variável op será executado com os parâmetros n1 e n2. O resultado n obtido será o do último método executado. Não é possível obter os resultados produzidos por todos os métodos. Por este motivo, se se registar uma lista de métodos numa função delegada, estes devolvem, na maioria das vezes, 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 devolve 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 nele registar funções
            // regista-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();
            // regista-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);
            // regista-se no delegado o método «subtrair» de c1
            op = c1.Soustraire;
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            //Registo de duas funções no delegado
            op = c1.Ajouter;
            op += c1.Soustraire;
            // execução do objeto delegado
            op(0, 0);
            // retirada 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 devolve um resultado do tipo int
  • linhas 9-12: o método de instância Ajouter tem a assinatura do delegado Opération.
  • linhas 14-17: o método de instância Soustraire tem a assinatura do delegado Opération.
  • linhas 20-23: o método de classe Augmenter tem 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. Contém uma lista de métodos com a assinatura do tipo delegado Opération. É-lhe atribuída uma primeira referência de método, a do método estático Class1.Augmenter.
  • linha 31: o delegate op é executado: serão executados todos os métodos referenciados pelo op. 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. Pretendia-se demonstrar que isso não tinha importância.
  • linha 39: o delegate op é executado: o método Ajouter será executado com os parâmetros passados para o delegate op.
  • linha 42: faz-se o mesmo com o método de instância Soustraire.
  • linhas 46-47: colocamos os métodos Ajouter e Soustraire 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 o 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:

1
2
3
4
5
6
7
8
9
Augmenter(4,7)
n=18
Ajouter(2,3)
n=5
Soustraire(2,3)
n=-1
Ajouter(0,0)
Soustraire(0,0)
Ajouter(1,1)

8.3. Delegados ou interfaces?

Os conceitos de delegates e de interfaces podem parecer bastante semelhantes e podemos questionar-nos sobre quais são, exatamente, as diferenças entre estes dois conceitos. Tomemos o seguinte exemplo, semelhante a um já estudado:


using System;
namespace Chap6 {
    class Program1 {
        // definição de um protótipo de função
        // aceita 2 inteiros como parâmetros e devolve 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));
            // Retirar uma função do delegado
            op -= Soustraire;
            // executar 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. Isto permite passar para o método Execute diferentes métodos (linhas 26, 28, 32 e 36). Esta 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 método único 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 aspeto polimórfico que o parâmetro do tipo delegate apresentava no exemplo anterior. Os dois exemplos mostram, ao mesmo tempo, as diferenças entre estes dois conceitos.

Os tipos delegate e interface são intercambiáveis

  • se a interface tiver apenas um método. Com efeito, o tipo delegate é um invólucro para um único método, enquanto a interface pode, por sua vez, definir vários métodos.
  • se o aspeto multicast do delegate não for utilizado. De facto, este conceito de multicast não existe na interface.

Se estas duas condições forem verificadas, então temos a escolha entre as duas assinaturas seguintes 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 revelar-se mais flexível de utilizar. Com efeito, na primeira assinatura, o primeiro parâmetro do método deve implementar a interface IOperation. Isto 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 é necessário criar nada adicional.

8.4. Gestão de eventos

Os objetos delegate podem ser utilizados para definir eventos. Uma classe C1 pode definir um evento evt da seguinte forma:

  • um tipo delegate é definido dentro ou fora da classe C1:
delegate TResult Evt(T1 param1, T2 param2, ...);
  • a classe C1 define um campo do tipo delegate Evt:
public Evt Evt1;
  • quando uma instância c1 da classe C1 pretender sinalizar um evento, executará o seu delegado Evt1, passando-lhe os parâmetros definidos pelo delegado Evt. Todos os métodos registados no delegate Evt1 serão então executados com esses parâmetros. Pode dizer-se que foram notificados do evento Evt1.
  • Se um objeto c2 que utiliza um objeto c1 quiser ser notificado da ocorrência do evento Evt1 no objeto c1, registará um dos seus métodos c2.M no objeto delegado c1.Evt1 do objeto c1.. Assim, o seu método c2.M será executado sempre que oevento Evt1 ocorrer no objeto c1. Poderá também cancelar a inscrição quando já não quiser ser notificado sobre o evento.
  • Como o objeto delegado c1.Evt1 pode registar vários métodos, diferentes objetos ci poderão registar-se junto do delegado c1.Evt1 para serem notificados do evento Evt1 no c1.

Neste cenário, temos:

  • uma classe que sinaliza um evento
  • classes que são notificadas desse evento. Diz-se que estas subscrevem o evento ou se inscrevem no evento.
  • um tipo delegate que define a assinatura dos métodos que serão notificados do evento

O framework .NET define:

  • uma assinatura padrão do delegate de um evento
public delegate void MyEventHandler(object source, EventArgs evtInfo);
  • source: o objeto que sinalizou o evento
  • evtInfo: um objeto do tipo EventArgs ou derivado que fornece informações sobre o evento
  • o nome do delegate deve terminar em EventHandler
  • uma forma padrão de declarar um evento do tipo MyEventHandler numa classe:
1
2
3
4
public Class C1{
    public event MyEventHandler Evt1;
...
}

O campo Evt1 é do tipo delegate. A palavra-chave event existe para restringir as operações que podem ser realizadas sobre ele:

  • fora da classe C1, apenas as operações += e -= são possíveis. Isto impede a eliminação (por exemplo, por erro do programador) dos métodos subscritos ao evento. É possível simplesmente subscrever (+=) ou cancelar a subscrição (-=) do evento.
  • apenas uma instância do tipo C1 pode executar a chamada Evt1(source,evtInfo), que desencadeia a execução dos métodos subscritos ao evento Evt1.

O framework .NET fornece um método genérico que satisfaz a 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 do seu segundo parâmetro
  • o tipo TEventArgs deve derivar do tipo EventsArgs (onde TEventArgs: EventArgs)

Com este delegate genérico, a declaração de um evento X na classe C seguirá o seguinte esquema recomendado:

  • 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
protected void OnXHandler(XEventArgs e);

destinado a «publicar» o evento X aos assinantes.

Consideremos o seguinte exemplo:

  • uma classe Emetteur encapsula uma temperatura. Esta temperatura é monitorizada. Quando esta temperatura ultrapassa um determinado limiar, deve ser lançado um evento. Chamaremos a este evento TemperatureTropHaute. As informações sobre este evento serão encapsuladas num tipo TemperatureTropHauteEventArgs.
  • Uma classe Souscripteur subscreve o evento anterior. Quando é notificada do evento, apresenta uma mensagem na consola.
  • Um programa de consola cria um emissor e dois subscritores. Introduz as temperaturas através do teclado e regista-as numa instância Emetteur. Se esta for demasiado elevada, a instância Emetteur publica o evento TemperatureTropHaute.

Para estar em conformidade com o método recomendado de gestão de eventos, definimos, em primeiro lugar, 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 comunicado
        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) {
            // envio do evento TemperatureTropHaute aos assinantes
            TemperatureTropHaute(this, evt);
        }
    }
}
  • linha 5: o limiar de temperatura a partir do qual o evento TemperatureTropHaute será publicado.
  • linha 10: o emissor tem 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 regista. É o método set que faz com que o evento TemperatureTropHaute seja publicado se a temperatura a registar ultrapassar o limiar da linha 5. Este método faz com que o evento seja publicado pelo método OnTemperatureTropHauteHandler da linha 29, passando-lhe como parâmetro um objeto TemperatureTropHauteEventArgs no qual foi registada a temperatura que excedeu o limiar.
  • linhas 29-32: o evento TemperatureTropHaute é publicado com o próprio emissor como primeiro parâmetro e o objeto TemperatureTropHauteEventArgs recebido como parâmetro como segundo parâmetro.

A classe Souscripteur, que se vai subscrever ao evento TemperatureTropHaute, é a seguinte:


using System;

namespace Chap6 {
    public class Souscripteur {
        // nome
        public string Nom { get; set; }

        // gestor do evento TemperatureTropHaute
        public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
            // exibição na consola 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 subscritor é identificado por um nome.
  • linhas 9-12: o método que será associado ao evento TemperatureTropHaute. Tem a assinatura do tipo delegate EventHandler<TEventArgs> que um gestor de eventos deve possuir. O método apresenta na consola: o nome do subscritor que exibe a mensagem, o nome do emissor que sinalizou o evento e a temperatura que o desencadeou.
  • A subscrição do evento TemperatureTropHaute de um objeto Emetteur não é efetuada na classe Souscripteur. Será efetuada por uma classe externa.

O programa [Program.cs] interliga todos estes 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 subscritores
            Souscripteur[] souscripteurs = new Souscripteur[2];
            for (int i = 0; i < souscripteurs.Length; i++) {
                // criação de um subscritor
                souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
                // subscreve-se o evento TemperatureTropHaute de e1
                e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
            }
            // leitura das temperaturas através do teclado
            decimal temperature;
            Console.Write("Température (rien pour arrêter) : ");
            string saisie = Console.ReadLine().Trim();
            // desde que a linha introduzida não esteja vazia
            while (saisie != "") {
                // a entrada é um número decimal?
                if (decimal.TryParse(saisie, out temperature)) {
                    // temperatura correta — regista-se
                    e1.Temperature = temperature;
                } else {
                    // é assinalado o erro
                    Console.WriteLine("Température incorrecte");
                }
                // nova introdução
                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 subscritores que são subscritos ao evento TemperatureTropHaute do emissor.
  • linhas 20-32: ciclo de introdução das temperaturas através do teclado
  • linha 24: se a temperatura introduzida estiver correta, é transmitida ao objeto Emetteur e1, que acionará o evento TemperatureTropHaute se a temperatura for superior a 19 °C.

Os resultados da execução são os seguintes:

1
2
3
4
Température (rien pour arrêter) : 17
Température (rien pour arrêter) : 21
Souscripteur [s0] : la source [e] a signalé une température trop haute : [21]
Souscripteur [s1] : la source [e] a signalé une température trop haute : [21]