Comprensión de eventos y controladores de eventos en C#
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?
Para comprender los controladores de eventos, es necesario comprender a los delegados . En C# , puedes considerar 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 devuelven void
y toman un object
y EventArgs
. Algo así 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 ello 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 a llamar cuando se genera el evento.* Por supuesto, para poder llamar a un método, necesitamos saber qué argumentos pasarle. Usamos el delegado como "contrato" entre el evento y todos los métodos específicos que serán llamados.
Entonces, el valor predeterminado EventHandler
(y muchos similares) representa una forma específica de método (nuevamente, void/object-EventArgs). Cuando declaras un evento, estás diciendo qué forma de método (EventHandler) invocará ese evento, especificando 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, bajo las sábanas, solo una lista de métodos de la misma "forma". La lista se almacena donde vive el evento. Cuando el evento se "genera", en realidad es simplemente "revisar esta lista de métodos y llamar a cada uno, usando estos valores como parámetros". Asignar un controlador de eventos es una forma más bonita y sencilla de agregar su método a esta lista de métodos. ser llamado).
C# conoce dos términos delegate
y event
. Empecemos por el primero.
Delegar
A delegate
es una referencia a un método. Así como puedes crear una referencia a una instancia:
MyClass instance = myFactory.GetInstance();
Puede utilizar 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 mediante la referencia:
MyClass instance = myMethod();
¿Pero por qué lo harías? También puedes simplemente llamar myFactory.GetInstance()
directamente. En este caso puedes. Sin embargo, hay muchos casos en los que no desea que el resto de la aplicación tenga conocimiento myFactory
o llame myFactory.GetInstance()
directamente.
Una opción obvia es si desea poder reemplazar myFactory.GetInstance()
desde myOfflineFakeFactory.GetInstance()
un lugar central (también conocido como patrón de método de fábrica ).
Patrón de método de fábrica
Entonces, si tienes una TheOtherClass
clase y necesita usar myFactory.GetInstance()
, así es como se verá el código sin delegados (deberás informar TheOtherClass
sobre el tipo de tu myFactory
):
TheOtherClass toc;
//...
toc.SetFactory(myFactory);
class TheOtherClass
{
public void SetFactory(MyFactory factory)
{
// set here
}
}
Si usaría delegados, no es necesario que exponga 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 darle un delegado a alguna otra clase para que lo use, sin exponer su tipo a ellos. Lo único que estás exponiendo es la firma de tu método (cuántos parámetros tienes y demás).
"Firma de mi método", ¿dónde escuché eso antes? Oh si, interfaces!!! Las interfaces describen la firma de una clase completa. ¡Piense en los delegados como si describieran la firma de un solo método!
Otra gran diferencia entre una interfaz y un delegado es que cuando escribes tu clase, no tienes que decirle a C# "este método implementa ese tipo de delegado". Con las interfaces, es necesario 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 llame al delegado, se ejecutarán múltiples métodos adjuntos explícitamente. Una referencia a un objeto siempre sólo puede hacer referencia a un objeto.
Las restricciones para a MulticastDelegate
son que la firma (método/delegado) no debe tener ningún valor de retorno ( void
) y las palabras clave out
no ref
se utilizan en la firma. Obviamente, no se puede llamar a dos métodos que devuelven un número y esperar que devuelvan el mismo número. Una vez que cumple la firma, el delegado es automáticamente un MulticastDelegate
.
Evento
Los eventos son solo propiedades (como las propiedades get;set; de los campos de instancia) que exponen la suscripción al delegado desde 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 haga referencia a ellos, y somos un botón de UI, entonces: Puedo pedirle a cualquiera que esté interesado en si me hicieron clic, que registre su método con nosotros (a través del evento que expusimos). Podemos utilizar todos los métodos que nos dieron y hacer referencia a ellos por nuestro delegado. Y luego, esperaremos y esperaremos... hasta que un usuario venga y haga clic en ese botón, entonces tendremos motivos suficientes para invocar al delegado. Y debido a que el delegado hace referencia a todos los métodos que se nos han proporcionado, todos esos métodos serán invocados. No sabemos qué hacen esos métodos, ni sabemos qué clase los implementa. Lo único que nos importa es que alguien estuviera interesado en que hicieran clic en nosotros y nos dio una referencia de un método que cumplía con nuestra firma deseada.
Java
Los lenguajes como Java no tienen delegados. En su lugar, utilizan interfaces. La forma en que lo hacen es pedirle a cualquiera que esté interesado en que "nos hagan clic" que implemente una determinada interfaz (con un determinado método que podamos llamar) y luego nos proporcione 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 al que podemos llamar' cada vez que hacemos clic.
En realidad, esa es la declaración de un controlador de eventos: un método que será llamado 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!");
}
Cada vez que Foo
se dispara MyEvent
, OnMyEvent
se llamará a su controlador.
No siempre es necesario utilizar una instancia de EventArgs
como segundo parámetro. Si desea incluir información adicional, puede utilizar una clase derivada de EventArgs
( EventArgs
es la base por convención). Por ejemplo, si observa algunos de los eventos definidos Control
en WinForms o FrameworkElement
en WPF, puede ver ejemplos de eventos que pasan información adicional a los controladores de eventos.
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();
}
}
}
Solo para agregar a las excelentes respuestas existentes aquí, basándose en el código aceptado, que usa 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 los controladores también se pueden cancelar del registro -=
de esta manera:
// -= removes the handler from the event's list of "listeners":
myObj.SomethingHappened -= HandleSomethingHappened;
Para completar, generar el 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!");
}
La copia local del controlador es necesaria para garantizar que la invocación sea segura para los subprocesos; de lo contrario, un subproceso podría ir y cancelar el registro del último controlador para el evento inmediatamente después de que comprobamos si lo era null
, y nos divertiríamos NullReferenceException
allí . .
C# 6 introdujo una agradable abreviatura para este patrón. Utiliza el operador de propagación nula.
SomethingHappened?.Invoke("Hi there!");