Comprender eventos y controladores de eventos en C #

330

Entiendo el propósito de los eventos, especialmente en el contexto de la creación de interfaces de usuario. Creo que este es el prototipo para crear un evento:

public void EventName(object sender, EventArgs e);

¿Qué hacen los controladores de eventos, por qué son necesarios y cómo puedo crear uno?

Levi Campbell
fuente
10
Como señaló @Andy, el fragmento de código aquí describe el método registrado para el evento, no el evento en sí.
dthrasher

Respuestas:

661

Para comprender los controladores de eventos, debe comprender a los delegados . En C # , puede pensar en un delegado como un puntero (o una referencia) a un método. Esto es útil porque el puntero se puede pasar como un valor.

El concepto central de un delegado es su firma o forma. Es decir (1) el tipo de retorno y (2) los argumentos de entrada. Por ejemplo, si creamos un delegado void MyDelegate(object sender, EventArgs e), solo puede apuntar a métodos que regresan void, y tomar un objectyEventArgs . Como un agujero cuadrado y una clavija cuadrada. Entonces, decimos que estos métodos tienen la misma firma o forma que el delegado.

Entonces, sabiendo cómo crear una referencia a un método, pensemos en el propósito de los eventos: queremos hacer que se ejecute algún código cuando algo sucede en otra parte del sistema, o "manejar el evento". Para hacer esto, creamos métodos específicos para el código que queremos que se ejecute. El pegamento entre el evento y los métodos a ejecutar son los delegados. El evento debe almacenar internamente una "lista" de punteros a los métodos para llamar cuando se genera el evento. * Por supuesto, para poder llamar a un método, ¡necesitamos saber qué argumentos pasarle! Usamos al delegado como el "contrato" entre el evento y todos los métodos específicos que se llamarán.

Entonces, el valor predeterminado EventHandler(y muchos similares) representa una forma específica del método (nuevamente, void / object-EventArgs). Cuando declaras un evento, estás diciendo qué forma de método (EventHandler) invocará ese evento, al especificar un delegado:

//This delegate can be used to point to methods
//which return void and take a string.
public delegate void MyEventHandler(string foo);

//This event can cause any method which conforms
//to MyEventHandler to be called.
public event MyEventHandler SomethingHappened;

//Here is some code I want to be executed
//when SomethingHappened fires.
void HandleSomethingHappened(string foo)
{
    //Do some stuff
}

//I am creating a delegate (pointer) to HandleSomethingHappened
//and adding it to SomethingHappened's list of "Event Handlers".
myObj.SomethingHappened += new MyEventHandler(HandleSomethingHappened);

//To raise the event within a method.
SomethingHappened("bar");

(* Esta es la clave de los eventos en .NET y elimina la "magia": un evento es, en realidad, solo una lista de métodos de la misma "forma". La lista se almacena donde vive el evento. el evento se "genera", en realidad es solo "ir a través de esta lista de métodos y llamar a cada uno, utilizando estos valores como parámetros". Asignar un controlador de eventos es solo una forma más bonita y fácil de agregar su método a esta lista de métodos ser llamado).

Rex M
fuente
24
¿Y ahora alguien puede explicar por qué el evento se llama EventHandler? De todas las convenciones de nomenclatura confusa, este es el peor ...
Joel en Gö
37
@Joel en Go, el evento no se llama EventHandler - EventHandler es el contrato que el evento debe tener con cualquiera que se comunique con él. Es como "string MyString": la cadena declara el tipo. evento MyEventHandler TheEvent declara que cualquier persona que interactúe con este evento debe cumplir con el contrato MyEventHandler. La convención del controlador se debe a que el contrato describe principalmente cómo manejar el evento.
Rex M
18
¿Cómo se dispara el evento?
alquímico
17
@Rex M: gracias por la primera explicación coherente para "MyEventHandler" que he visto :)
Joel en Gö
10
Gracias por la fase: "El pegamento entre el evento y los métodos a ejecutar son los delegados", esto es realmente increíble.
zionpi
103

C # conoce dos términos, delegatey event. Comencemos con el primero.

Delegar

A delegatees una referencia a un método. Al igual que puede crear una referencia a una instancia:

MyClass instance = myFactory.GetInstance();

Puede usar un delegado para crear una referencia a un método:

Action myMethod = myFactory.GetInstance;

Ahora que tiene esta referencia a un método, puede llamar al método a través de la referencia:

MyClass instance = myMethod();

¿Pero por qué lo harías? También puedes llamar myFactory.GetInstance()directamente. En este caso puedes. Sin embargo, hay muchos casos para pensar en los que no desea que el resto de la aplicación tenga conocimiento myFactoryo llamemyFactory.GetInstance() directamente.

Una obvia es si usted quiere ser capaz de reemplazar myFactory.GetInstance()a myOfflineFakeFactory.GetInstance()partir de un lugar central (aka Factory Method ).

Patrón de método de fábrica

Entonces, si tiene una TheOtherClassclase y necesita usarla myFactory.GetInstance(), así es como se verá el código sin delegados (deberá informar TheOtherClasssobre el tipo de su myFactory):

TheOtherClass toc;
//...
toc.SetFactory(myFactory);


class TheOtherClass
{
   public void SetFactory(MyFactory factory)
   {
      // set here
   }

}

Si usaría delegados, no tiene que exponer el tipo de mi fábrica:

TheOtherClass toc;
//...
Action factoryMethod = myFactory.GetInstance;
toc.SetFactoryMethod(factoryMethod);


class TheOtherClass
{
   public void SetFactoryMethod(Action factoryMethod)
   {
      // set here
   }

}

Por lo tanto, puede asignar un delegado a otra clase para que lo use, sin exponer su tipo a ellos. Lo único que está exponiendo es la firma de su método (cuántos parámetros tiene y tal).

"Firma de mi método", ¿dónde escuché eso antes? ¡Oh sí, interfaces! Las interfaces describen la firma de toda una clase. ¡Piense en los delegados como describiendo la firma de un solo método!

Otra gran diferencia entre una interfaz y un delegado es que cuando escribe su clase, no tiene que decirle a C # "este método implementa ese tipo de delegado". Con las interfaces, debe decir "esta clase implementa ese tipo de interfaz".

Además, una referencia de delegado puede (con algunas restricciones, ver más abajo) hacer referencia a múltiples métodos (llamados MulticastDelegate ). Esto significa que cuando llama al delegado, se ejecutarán varios métodos adjuntos explícitamente. Una referencia de objeto siempre puede hacer referencia a un solo objeto.

Las restricciones para a MulticastDelegateson que la firma (método / delegado) no debe tener ningún valor de retorno ( void) y las palabras clave outy refno se utiliza en la firma. Obviamente, no puede llamar a dos métodos que devuelven un número y esperan que devuelvan el mismo número. Una vez que la firma cumple, el delegado es automáticamente a MulticastDelegate.

Evento

Los eventos son solo propiedades (como get; set; propiedades para campos de instancia) que exponen la suscripción al delegado de otros objetos. Estas propiedades, sin embargo, no admiten get; set ;. En cambio, admiten agregar; eliminar;

Entonces puedes tener:

    Action myField;

    public event Action MyProperty
    {
        add { myField += value; }
        remove { myField -= value; }
    }

Uso en la interfaz de usuario (WinForms, WPF, UWP, etc.)

Entonces, ahora sabemos que un delegado es una referencia a un método y que podemos tener un evento para que el mundo sepa que pueden darnos sus métodos para que nuestro delegado nos haga referencia, y somos un botón de interfaz de usuario, entonces: puede preguntar a cualquier persona interesada en si se me hizo clic, para registrar su método con nosotros (a través del evento que expusimos). Podemos usar todos los métodos que nos fueron proporcionados y hacer referencia a ellos por nuestro delegado. Y luego, esperaremos y esperaremos ... hasta que aparezca un usuario y haga clic en ese botón, entonces tendremos suficientes razones para invocar al delegado. Y debido a que el delegado hace referencia a todos los métodos que nos han dado, se invocarán todos esos métodos. No sabemos qué hacen esos métodos, ni sabemos qué clase implementa esos métodos. Lo único que nos importa es que alguien esté interesado en que se haga clic en nosotros,

Java

Lenguajes como Java no tienen delegados. Usan interfaces en su lugar. La forma en que lo hacen es pedirle a cualquiera que esté interesado en 'que se nos haga clic', que implemente una determinada interfaz (con un cierto método al que podamos llamar), y luego darnos la instancia completa que implementa la interfaz. Mantenemos una lista de todos los objetos que implementan esta interfaz y podemos llamar a su 'cierto método que podemos llamar' cada vez que hacemos clic.

tofi9
fuente
aplaude la explicación, pero ¿en qué se diferencia un evento de una instancia de un delegado que acepta suscriptores? ambos se ven exactamente igual?
BKSpurgeon
@BKSpurgeon eso se debe a que son "delegados que se hacen cargo de los suscriptores", eventes una simple sintaxis, nada más.
Mathieu Guindon
"Las restricciones para un MulticastDelegate son que la firma (método / delegado) no debe tener ningún valor de retorno (nulo)", no creo que esto sea correcto. Si tienen valores de retorno, devolverá el último.
Hozikimaru
"Por lo tanto, puede dar un delegado a otra clase para que lo use, sin exponer su tipo a ellos. Lo único que está exponiendo es la firma de su método ..." , ese para mí es el punto crítico. ¡Gracias!
Ryan
40

Esa es en realidad la declaración de un controlador de eventos, un método que se llamará cuando se active un evento. Para crear un evento, escribirías algo como esto:

public class Foo
{
    public event EventHandler MyEvent;
}

Y luego puedes suscribirte al evento así:

Foo foo = new Foo();
foo.MyEvent += new EventHandler(this.OnMyEvent);

Con OnMyEvent () definido así:

private void OnMyEvent(object sender, EventArgs e)
{
    MessageBox.Show("MyEvent fired!");
}

Siempre que se Foodispare MyEvent, se OnMyEventllamará a su controlador.

No siempre tiene que usar una instancia de EventArgscomo segundo parámetro. Si desea incluir información adicional, puede usar una clase derivada de EventArgs( EventArgses la base por convención). Por ejemplo, si observa algunos de los eventos definidos Controlen WinForms o FrameworkElementen WPF, puede ver ejemplos de eventos que pasan información adicional a los controladores de eventos.

Andy
fuente
14
Gracias por responder la pregunta y no entrar en Delegados y Eventos.
divide_byzero
3
Recomendaría no usar el OnXXXpatrón de nombres para su controlador de eventos. (Estúpidamente, se considera que OnXXX significa 'manejar XXX' en MFC y 'elevar XXX' en .net, por lo que ahora su significado no es claro y confuso; consulte esta publicación para obtener más detalles ). Los nombres preferidos serían RaiseXXXpara generar eventos y / HandleXXXo Sender_XXXpara controladores de eventos.
Jason Williams
1
¿Puedes mostrar un ejemplo de trabajo con una aplicación WinForms simple?
MC9000
40

Aquí hay un ejemplo de código que puede ayudar:

using System;
using System.Collections.Generic;
using System.Text;

namespace Event_Example
{
  // First we have to define a delegate that acts as a signature for the
  // function that is ultimately called when the event is triggered.
  // You will notice that the second parameter is of MyEventArgs type.
  // This object will contain information about the triggered event.

  public delegate void MyEventHandler(object source, MyEventArgs e);

  // This is a class which describes the event to the class that receives it.
  // An EventArgs class must always derive from System.EventArgs.

  public class MyEventArgs : EventArgs
  {
    private string EventInfo;

    public MyEventArgs(string Text) {
      EventInfo = Text;
    }

    public string GetInfo() {
      return EventInfo;
    }
  }

  // This next class is the one which contains an event and triggers it
  // once an action is performed. For example, lets trigger this event
  // once a variable is incremented over a particular value. Notice the
  // event uses the MyEventHandler delegate to create a signature
  // for the called function.

  public class MyClass
  {
    public event MyEventHandler OnMaximum;

    private int i;
    private int Maximum = 10;

    public int MyValue
    {
      get { return i; }
      set
      {
        if(value <= Maximum) {
          i = value;
        }
        else 
        {
          // To make sure we only trigger the event if a handler is present
          // we check the event to make sure it's not null.
          if(OnMaximum != null) {
            OnMaximum(this, new MyEventArgs("You've entered " +
              value.ToString() +
              ", but the maximum is " +
              Maximum.ToString()));
          }
        }
      }
    }
  }

  class Program
  {
    // This is the actual method that will be assigned to the event handler
    // within the above class. This is where we perform an action once the
    // event has been triggered.

    static void MaximumReached(object source, MyEventArgs e) {
      Console.WriteLine(e.GetInfo());
    }

    static void Main(string[] args) {
      // Now lets test the event contained in the above class.
      MyClass MyObject = new MyClass();
      MyObject.OnMaximum += new MyEventHandler(MaximumReached);
      for(int x = 0; x <= 15; x++) {
        MyObject.MyValue = x;
      }
      Console.ReadLine();
    }
  }
}
Gary Willoughby
fuente
44
La invocación de delegados se puede simplificar en C # 6 a:OnMaximum?.Invoke(this,new MyEventArgs("you've entered..."));
Tim Schmelter
23

Solo para agregar a las excelentes respuestas existentes aquí: construir sobre el código en el aceptado, que utiliza un delegate void MyEventHandler(string foo) ...

Como el compilador conoce el tipo de delegado del evento SomethingHappened , esto:

myObj.SomethingHappened += HandleSomethingHappened;

Es totalmente equivalente a:

myObj.SomethingHappened += new MyEventHandler(HandleSomethingHappened);

Y manipuladores también puede ser registrado con -=la siguiente manera:

// -= removes the handler from the event's list of "listeners":
myObj.SomethingHappened -= HandleSomethingHappened;

En aras de la exhaustividad, el aumento del evento se puede hacer de esta manera, solo en la clase propietaria del evento:

//Firing the event is done by simply providing the arguments to the event:
var handler = SomethingHappened; // thread-local copy of the event
if (handler != null) // the event is null if there are no listeners!
{
    handler("Hi there!");
}

Se necesita la copia local del subproceso del controlador para asegurarse de que la invocación es segura para el subproceso; de lo contrario, un subproceso podría ir y anular el registro del último controlador para el evento inmediatamente después de comprobar si era así null, y nos divertiríamosNullReferenceException existe .


C # 6 introdujo una bonita mano corta para este patrón. Utiliza el operador de propagación nulo.

SomethingHappened?.Invoke("Hi there!");
Mathieu Guindon
fuente
13

Mi comprensión de los eventos es;

Delegar:

Una variable para contener la referencia al método / métodos a ejecutar. Esto hace posible pasar métodos como una variable.

Pasos para crear y llamar al evento:

  1. El evento es una instancia de un delegado.

  2. Como un evento es una instancia de un delegado, primero tenemos que definir el delegado.

  3. Asigne el método / métodos que se ejecutarán cuando se active el evento ( Llamar al delegado )

  4. Dispara el evento ( llama al delegado )

Ejemplo:

using System;

namespace test{
    class MyTestApp{
        //The Event Handler declaration
        public delegate void EventHandler();

        //The Event declaration
        public event EventHandler MyHandler;

        //The method to call
        public void Hello(){
            Console.WriteLine("Hello World of events!");
        }

        public static void Main(){
            MyTestApp TestApp = new MyTestApp();

            //Assign the method to be called when the event is fired
            TestApp.MyHandler = new EventHandler(TestApp.Hello);

            //Firing the event
            if (TestApp.MyHandler != null){
                TestApp.MyHandler();
            }
        }

    }   

}
KE50
fuente
3

editor: donde ocurren los eventos. El editor debe especificar qué delegado está usando la clase y generar los argumentos necesarios, pasar esos argumentos y a sí mismo al delegado.

suscriptor: donde ocurre la respuesta. El suscriptor debe especificar métodos para responder a los eventos. Estos métodos deben tomar el mismo tipo de argumentos que el delegado. El suscriptor luego agrega este método al delegado del editor.

Por lo tanto, cuando el evento ocurra en el editor, el delegado recibirá algunos argumentos del evento (datos, etc.), pero el editor no tiene idea de lo que sucederá con todos estos datos. Los suscriptores pueden crear métodos en su propia clase para responder a eventos en la clase del editor, de modo que los suscriptores puedan responder a los eventos del editor.

rileyss
fuente
2
//This delegate can be used to point to methods
//which return void and take a string.
public delegate void MyDelegate(string foo);

//This event can cause any method which conforms
//to MyEventHandler to be called.
public event MyDelegate MyEvent;

//Here is some code I want to be executed
//when SomethingHappened fires.
void MyEventHandler(string foo)
{
    //Do some stuff
}

//I am creating a delegate (pointer) to HandleSomethingHappened
//and adding it to SomethingHappened's list of "Event Handlers".
myObj.MyEvent += new MyDelegate (MyEventHandler);
Bilgi Sayar
fuente
0

Estoy de acuerdo con KE50, excepto que veo la palabra clave 'evento' como un alias para 'ActionCollection' ya que el evento contiene una colección de acciones a realizar (es decir, el delegado).

using System;

namespace test{

class MyTestApp{
    //The Event Handler declaration
    public delegate void EventAction();

    //The Event Action Collection 
    //Equivalent to 
    //  public List<EventAction> EventActions=new List<EventAction>();
    //        
    public event EventAction EventActions;

    //An Action
    public void Hello(){
        Console.WriteLine("Hello World of events!");
    }
    //Another Action
    public void Goodbye(){
        Console.WriteLine("Goodbye Cruel World of events!");
    }

    public static void Main(){
        MyTestApp TestApp = new MyTestApp();

        //Add actions to the collection
        TestApp.EventActions += TestApp.Hello;
        TestApp.EventActions += TestApp.Goodbye;

        //Invoke all event actions
        if (TestApp.EventActions!= null){
            //this peculiar syntax hides the invoke 
            TestApp.EventActions();
            //using the 'ActionCollection' idea:
            // foreach(EventAction action in TestApp.EventActions)
            //     action.Invoke();
        }
    }

}   

}
usuario3902302
fuente
0

Grandes respuestas técnicas en el post! Técnicamente no tengo nada que agregar a eso.

¡Una de las razones principales por las que aparecen nuevas funciones en los idiomas y el software en general es el marketing o la política de la empresa! :-) ¡Esto no debe ser subestimado!

¡Creo que esto también se aplica a ciertos delegados y eventos! Los encuentro útiles y agrego valor al lenguaje C #, pero por otro lado, ¡el lenguaje Java decidió no usarlos! decidieron que cualquier cosa que esté resolviendo con delegados ya puede resolver con las características existentes del lenguaje, es decir, interfaces, por ejemplo

Ahora, alrededor de 2001, Microsoft lanzó el .NET Framework y el lenguaje C # como una solución de la competencia para Java, por lo que fue bueno tener NUEVAS CARACTERÍSTICAS que Java no tiene.

Siraf
fuente
0

Recientemente hice un ejemplo de cómo usar eventos en C # y lo publiqué en mi blog. Traté de hacerlo lo más claro posible, con un ejemplo muy simple. En caso de que pueda ayudar a alguien, aquí está: http://www.konsfik.com/using-events-in-csharp/

Incluye la descripción y el código fuente (con muchos comentarios), y se centra principalmente en el uso adecuado (como plantilla) de eventos y controladores de eventos.

Algunos puntos clave son:

  • Los eventos son como "subtipos de delegados", solo que más restringidos (en el buen sentido). De hecho, la declaración de un evento siempre incluye un delegado (los EventHandlers son un tipo de delegado).

  • Los controladores de eventos son tipos específicos de delegados (puede pensar en ellos como una plantilla), que obligan al usuario a crear eventos que tienen una "firma" específica. La firma tiene el formato: (remitente del objeto, EventArgs eventarguments).

  • Puede crear su propia subclase de EventArgs para incluir cualquier tipo de información que el evento necesite transmitir. No es necesario usar EventHandlers cuando se usan eventos. Puede omitirlos por completo y usar su propio tipo de delegado en su lugar.

  • Una diferencia clave entre el uso de eventos y delegados es que los eventos solo se pueden invocar desde la clase en la que se declararon, aunque se puedan declarar como públicos. Esta es una distinción muy importante, ya que permite que sus eventos estén expuestos para que estén "conectados" a métodos externos, mientras que al mismo tiempo están protegidos del "mal uso externo".

konsfik
fuente
0

Otra cosa que debe saber , en algunos casos, debe usar los Delegados / Eventos cuando necesita un bajo nivel de acoplamiento !

Si desea usar un componente en varios lugares de la aplicación , debe hacer un componente con un bajo nivel de acoplamiento y la LÓGICA específica no importa debe delegarse FUERA de su componente. Esto garantiza que tenga un sistema desacoplado y un código más limpio.

En principio SÓLIDO esta es la " D ", ( D principio de inversión de dependencia ).

También conocido como " IoC ", Inversión de control .

Puede hacer " IoC " con Eventos, Delegados y DI (Inyección de dependencia).

Es fácil acceder a un método en una clase secundaria. Pero es más difícil acceder a un método en una clase padre desde niño. ¡Tienes que pasar la referencia del padre al hijo! (o use DI con interfaz)

¡Delegados / Eventos nos permite comunicarnos del niño al padre sin referencia!

ingrese la descripción de la imagen aquí

En este diagrama anterior, no utilizo Delegado / Evento y el componente primario B debe tener una referencia del componente primario A para ejecutar la lógica de negocios no preocupada en el método de A. (alto nivel de acoplamiento)

¡Con este enfoque, tendría que poner todas las referencias de todos los componentes que usan el componente B! :(

ingrese la descripción de la imagen aquí

En este diagrama anterior, uso Delegado / Evento y el componente B no tiene que conocer A. (bajo nivel de acoplamiento)

¡Y puede usar su componente B en cualquier lugar de su aplicación !

A. Morel
fuente