Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Los eventos de .NET generalmente siguen unos patrones conocidos. La estandarización de estos patrones significa que los desarrolladores pueden aplicar conocimiento de esos patrones estándar, que se pueden aplicar a cualquier programa de eventos de .NET.
Veamos estos patrones estándar para asegurarse de que tiene todos los conocimientos necesarios para crear fuentes de eventos estándar y suscribirse y gestionar eventos estándar en su código.
Firmas de delegado de eventos
La firma estándar de un delegado de eventos de .NET es:
void EventRaised(object sender, EventArgs args);
Esta firma estándar proporciona información sobre en qué momento se utilizan los eventos:
- El tipo de valor devuelto es void. Los eventos pueden tener desde cero hasta muchas escuchas. Cuando se genera un evento se notifica a todos los oyentes. En general, los escuchadores no proporcionan valores en respuesta a eventos.
-
Los eventos indican el remitente: la firma del evento incluye el objeto que generó el evento. Esto proporciona a cualquier agente de escucha un mecanismo para comunicarse con el remitente. El tipo de tiempo de compilación de
sender
esSystem.Object
, aunque probablemente conozca un tipo más derivado que siempre será correcto. Por convención, useobject
. -
Los eventos empaquetan más información en una sola estructura: el
args
parámetro es un tipo derivado de System.EventArgs que incluye información adicional necesaria. (Verá en la sección siguiente que ya no se aplica esta convención). Si el tipo de evento no necesita más argumentos, debe proporcionar ambos argumentos. Hay un valor especial que debe usar EventArgs.Empty para indicar que tu evento no contiene información adicional.
Vamos a crear una clase que enumera los archivos en un directorio, o cualquiera de sus subdirectorios que siguen un patrón. Este componente genera un evento para cada archivo encontrado que coincida con el modelo.
El uso de un modelo de eventos proporciona algunas ventajas de diseño. Se pueden crear varios agentes de escucha de eventos que realicen acciones diferentes cuando se encuentre un archivo buscado. La combinación de los distintos agentes de escucha puede crear algoritmos más sólidos.
Esta es la declaración de argumento de evento inicial para buscar un archivo buscado:
public class FileFoundArgs : EventArgs
{
public string FoundFile { get; }
public FileFoundArgs(string fileName) => FoundFile = fileName;
}
Aunque este tipo parece un tipo pequeño exclusivo para datos, debe seguir la convención y convertirlo en un tipo de referencia (class
). Esto significa que el objeto de argumento se pasa por referencia y todos los suscriptores ven las actualizaciones de los datos. La primera versión es un objeto inmutable. Es preferible hacer que las propiedades en el tipo de argumento de evento sean inmutables. De este modo, un suscriptor no puede cambiar los valores antes de que otro suscriptor los vea. (Hay excepciones a esta práctica, como verá más adelante).
Después, debemos crear la declaración de evento en la clase FileSearcher. El uso del System.EventHandler<TEventArgs> tipo significa que no es necesario crear otra definición de tipo. Simplemente se puede usar una especialización genérica.
Vamos a rellenar la clase FileSearcher para buscar archivos que coincidan con un patrón y generar el evento correcto cuando se detecte una coincidencia.
public class FileSearcher
{
public event EventHandler<FileFoundArgs>? FileFound;
public void Search(string directory, string searchPattern)
{
foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
{
FileFound?.Invoke(this, new FileFoundArgs(file));
}
}
}
Definición y generación de eventos similares a campos
La manera más sencilla de agregar un evento a la clase es declarar ese evento como un campo público, como en el ejemplo anterior:
public event EventHandler<FileFoundArgs>? FileFound;
Parece que se está declarando un campo público, lo que podría parecer una práctica orientada a objetos incorrecta. Quiere proteger el acceso a los datos a través de propiedades o métodos. Aunque este código podría tener un aspecto incorrecto, el código generado por el compilador crea contenedores para que solo se pueda tener acceso a los objetos de evento de maneras seguras. Las únicas operaciones disponibles en un evento similar a un campo son agregar y quitar controladores:
var fileLister = new FileSearcher();
int filesFound = 0;
EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
Console.WriteLine(eventArgs.FoundFile);
filesFound++;
};
fileLister.FileFound += onFileFound;
fileLister.FileFound -= onFileFound;
Hay una variable local para el controlador. Si se ha usado el cuerpo de la expresión lambda, el controlador remove
no funcionará correctamente. Sería una instancia diferente del delegado y, en modo silencioso, no se hace nada.
El código fuera de la clase no puede generar el evento ni realizar ninguna otra operación.
A partir de C# 14, los eventos se pueden declarar como miembros parciales. Una declaración de evento parcial debe incluir una declaración de definición y una declaración de implementación. La declaración de definición debe usar la sintaxis de evento tipo campo. La declaración de implementación debe declarar los controladores add
y remove
.
Devolución de valores de suscriptores de eventos
La versión simple funciona correctamente. Vamos a agregar otra característica: la cancelación.
Al generar el evento Found, los oyentes deben poder detener el procesamiento adicional si este archivo es el último que se busca.
Los controladores de eventos no devuelven un valor, por lo que debe comunicarlo de otra manera. El patrón de eventos estándar usa el objeto EventArgs
para incluir campos que los suscriptores de eventos pueden usar para comunicar la cancelación.
Existen dos patrones diferentes que podrían usarse, basándose en la semántica del contrato de cancelación. En ambos casos, se agrega un campo booleano al evento EventArguments para el evento de archivo encontrado.
Uno de los patrones permitiría a cualquier suscriptor cancelar la operación. Para este patrón, el nuevo campo se inicializa en false
. Los suscriptores pueden cambiarlo a true
. Después de generar el evento para todos los abonados, el componente FileSearcher examina el valor booleano y toma las medidas correspondientes.
El segundo patrón solo debería cancelar la operación si todos los suscriptores quieren que se cancele. En este patrón, el nuevo campo se inicializa para indicar que se debe cancelar la operación y cualquier suscriptor puede modificarlo para indicar que la operación debe continuar. Después de que todos los abonados hayan visto el evento generado, el componente FileSearcher examina el valor booleano y toma las medidas correspondientes. Hay un paso adicional en este patrón: el componente debe saber si algún suscriptor respondió al evento. Si no hay ningún suscriptor, el campo indicaría incorrectamente una cancelación.
Vamos a implementar la primera versión de este ejemplo. Debe agregar un campo booleano denominado CancelRequested
al tipo FileFoundArgs
:
public class FileFoundArgs : EventArgs
{
public string FoundFile { get; }
public bool CancelRequested { get; set; }
public FileFoundArgs(string fileName) => FoundFile = fileName;
}
Este nuevo campo se inicializa automáticamente a false
para que no se cancele accidentalmente. El único otro cambio en el componente consiste en comprobar la marca después de la generación del evento para ver si alguno de los abonados ha solicitado una cancelación:
private void SearchDirectory(string directory, string searchPattern)
{
foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
{
var args = new FileFoundArgs(file);
FileFound?.Invoke(this, args);
if (args.CancelRequested)
break;
}
}
Una ventaja de este patrón es que no supone un cambio brusco. Ninguno de los suscriptores solicitó la cancelación antes y todavía no lo están. Ninguno del código de suscriptor requiere actualizaciones a menos que quieran admitir el nuevo protocolo de cancelación.
Vamos a actualizar el suscriptor para que solicite una cancelación una vez que encuentra el primer ejecutable:
EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
Console.WriteLine(eventArgs.FoundFile);
eventArgs.CancelRequested = true;
};
Adición de otra declaración de evento
Vamos a agregar una característica más y demostrar otras expresiones de lenguaje para los eventos. Vamos a agregar una sobrecarga del método Search
que recorre todos los subdirectorios en busca de archivos.
Este método podría ser una operación larga en un directorio con muchos subdirectorios. Vamos a agregar un evento que se genera cuando comienza cada nueva búsqueda en el directorio. Este evento permite a los suscriptores realizar un seguimiento del progreso y actualizar al usuario en cuanto a progreso. Todos los ejemplos que creó hasta ahora son públicos. Vamos a convertir este evento en un evento interno. Esto significa que también puede hacer que los tipos de argumento sean internos.
Para empezar, cree la nueva clase derivada EventArgs para notificar el nuevo directorio y el progreso.
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;
}
}
De nuevo, puede seguir las recomendaciones para crear un tipo de referencia inmutable para los argumentos de evento.
Después, defina el evento. Esta vez, se usa una sintaxis diferente. Además de usar la sintaxis de campos, puede crear de manera explícita la propiedad de evento con manejadores de adición y eliminación. En este ejemplo, no necesita código adicional en esos controladores, pero esto muestra cómo se crearían.
internal event EventHandler<SearchDirectoryArgs> DirectoryChanged
{
add { _directoryChanged += value; }
remove { _directoryChanged -= value; }
}
private EventHandler<SearchDirectoryArgs>? _directoryChanged;
De muchas maneras, el código que escribe aquí refleja el código que genera el compilador para las definiciones de eventos de campo que vio anteriormente. El evento se crea mediante la sintaxis similar a las propiedades. Tenga en cuenta que los controladores tienen nombres diferentes: add
y remove
. Se utilizan estos accesores para suscribirse al evento o anular la suscripción al evento. Tenga en cuenta que también debe declarar un campo de respaldo privado para almacenar la variable de evento. Esta variable se inicializa en null.
Después, se agregará la sobrecarga del método Search
que recorre los subdirectorios y genera los dos eventos. La manera más fácil es usar un argumento predeterminado para especificar que desea buscar en todos los directorios:
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)
{
_directoryChanged?.Invoke(this, new (dir, totalDirs, completedDirs++));
// Search 'dir' and its subdirectories for files that match the search pattern:
SearchDirectory(dir, searchPattern);
}
// Include the Current Directory:
_directoryChanged?.Invoke(this, new (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))
{
var args = new FileFoundArgs(file);
FileFound?.Invoke(this, args);
if (args.CancelRequested)
break;
}
}
En este punto, se puede ejecutar la aplicación mediante la llamada a la sobrecarga para buscar en todos los subdirectorios. No hay suscriptores en el nuevo DirectoryChanged
evento, pero el uso de la ?.Invoke()
expresión garantiza que funciona correctamente.
Vamos a agregar un controlador para escribir una línea que muestre el progreso en la ventana de la consola.
fileLister.DirectoryChanged += (sender, eventArgs) =>
{
Console.Write($"Entering '{eventArgs.CurrentSearchDirectory}'.");
Console.WriteLine($" {eventArgs.CompletedDirs} of {eventArgs.TotalDirs} completed...");
};
Ha visto patrones que se siguen en todo el ecosistema de .NET. Al aprender estos patrones y convenciones, escribe C# y .NET idiomáticos rápidamente.
Vea también
A continuación, verá algunos cambios en estos patrones en la versión más reciente de .NET.