Quién no se ha encontrado alguna vez con la necesidad de hacer una tarea que consume mucho tiempo ( acceso a ficheros, cálculos complejos, acceso a la red, etc ) y no quiere que su interfície de usuario se vea afectado por ello? No veo ninguna mano alzada, así que supondremos que es algo recurrente.
Este tipo de programación se ha llevado a cabo históricamente mediante hilos. Aunque .Net ha facilitado mucho la programación con ellos, hacer un uso de los mismos no deja de ser añadir complejidad al programa y es una fuente típica de errores. Es por esto que existen otras alternativas a la programación asíncrona sin tener que utilizar explícitamente hilos. Estas alternativas son las siguientes:
- Llamadas asíncronas a delegados.
- Llamadas asíncronas utilizando la interfaz IAsyncResult.
- El componente BackgroundWorker.
- Llamadas asíncronas basadas en eventos.
En este artículo nos centraremos en esta última aproximación ya que es la más completa y la más recomendada.
Este patrón se apoya en la utilización de dos clases de ayuda proporcionadas por .Net: la clase AsyncOperationManager que utilizaremos para crear instancias de la otra clase de ayuda, la AsyncOperation que utilizaremos para efectuar el seguimiento del progreso de la tarea asíncrona e informar del mismo. Una de las ventajas que tiene es que los eventos de progreso y cancelación se hace en el hilo adecuado, con lo que podemos actualizar la interfíce de usuario directamente, sin tener que recurrir a Invoke.
Vamos a ver paso a paso como crear una clase que implemente este patrón y como crear una clase cliente que la instancie y utilice.
Lo primero que tenemos que hacer es, obviamente, crear nuestra clase que queremos que se pueda llamar de manera asíncrona. En nuestro caso creamos una clase llamada LazyClass.
Seguidamente vamos a declarar los delegados y los eventos públicos que necesitamos para informar al cliente del progreso y terminación de la tarea asíncrona.
public delegate void LazyTaskProgressChangedEventHandler(LazyTaskProgressChangedEventArgs pArgs);
public delegate void LazyTaskCompletedEventHandler(object pSender, LazyTaskCompletedEventArgs pArgs);
public delegate void LazyTaskProgressChangedEventHandler(LazyTaskProgressChangedEventArgs pArgs);
public delegate void LazyTaskCompletedEventHandler(object pSender, LazyTaskCompletedEventArgs pArgs);
Esto nos obliga a definir los argumentos LazyTaskCompletedEventArgs y LazyTaskProgressChangedEventArgs.
La clase LazyTaskCompletedEventArgs tendrá la definición de las cosas que queremos que se informen al exterior cuando se haya terminado la tarea asíncrona. Esta clase puede ser tan compleja como queramos, aunque nosotros, por el bien del entendimiento del ejemplo, la hemos reducido lo máximo posible.
public class LazyTaskCompletedEventArgs : AsyncCompletedEventArgs
{
private int m_iNumIterations = int.MinValue;
private int m_iTotalTime = int.MinValue;
///
/// Constructor
///
public LazyTaskCompletedEventArgs(int pNumIterations, int pTotalTime, Exception pException, bool pCancelled, object pState) : base (pException, pCancelled, pState )
{
m_iNumIterations = pNumIterations;
m_iTotalTime = pTotalTime;
}
///
/// Número de iteraciones
///
public int NumIterations
{
get
{
RaiseExceptionIfNecessary();
return m_iNumIterations;
}
}
///
/// Tiempo transcurrido
///
public int TotalTime
{
get
{
RaiseExceptionIfNecessary();
return m_iTotalTime;
}
}
}
Como se puede ver, en cada una de las propiedades antes de devolver el valor se llama a una función ( del espacio de nombres System.ComponentModel ) llamada RaiseExceptionIfNecessary. Esta función se utiliza para evitar que alguien acceda a los valores de la clase si se ha producido algún error o se ha cancelado la invocación asíncrona. En tal caso, al acceder a cualquiera de las propiedades de la clase recibiremos una excepción del tipo InvalidOperationException.
La clase LazyTaskProgressChangedEventArgs derivará de la clase ProgressChangedEventArgs del espacio de nombres System.ComponentModel. En nuestro caso no le añadimos nada ( con el porcentaje tenemos suficiente ) pero se le podría añadir todo lo necesario para nuestra tarea en concreto.
///
/// Classe que define los argumentos del progreso de la tarea
///
public class LazyTaskProgressChangedEventArgs : ProgressChangedEventArgs
{
public LazyTaskProgressChangedEventArgs(int pPercentatge, object pState)
: base(pPercentatge, pState)
{
}
}
Lo siguiente es implementar los delegados privados que utilizaremos como puente para informar al cliente del progreso de la tarea. Estos delegados tendrán una firma del tipo SendOrPostCallback, definida también en el espacio de nombres System.ComponentModel.
private SendOrPostCallback onProgressDelegate;
private SendOrPostCallback onCompletedDelegate;
Y los inicializamos en el constructor de la clase:
onProgressDelegate = new SendOrPostCallback(LazyTaskProgress);
onCompletedDelegate = new SendOrPostCallback(LazyTaskCompleted);
A continuación vamos a declarar el delegado encargado de realizar el trabajo que queramos hacer en nuestra clase. Este delegado tendrá que recibir un parámetro del tipo AsyncOperation que se utilizará para hacer el seguimiento del progreso de la tarea asíncrona y un delegado de tipo SendOrPostCallback que se utilizará para notificar del fin de la operación. También puede recibir cualquier otro parámetro que necesitemos para la ejecución de la tarea.
private delegate void WorkerEventHandler ( AsyncOperation pOperation, SendOrPostCallback pCompletionMethod );
Después declararemos una colección para administrar las diferentes operaciones asíncronas que se produzcan en nuestra clase. Podemos utilizar un Dictionary, un HibrydDictionary o lo que más nos convenga.
ListDictionary m_ldTasks = new ListDictionary();
A continuación pasamos a implementar los eventos públicos que utilizaremos para informar al cliente del progreso y terminación de la tarea asíncrona.
///
/// Implementación del delegado del progreso
///
private void LazyTaskProgress(object pSate)
{
ProgressChangedEventArgs e = pSate as LazyTaskProgressChangedEventArgs;
FireOnLazyTaskProgressChanged(e);
}
///
/// Implementación del delegado de tarea completada
///
private void LazyTaskCompleted(object pSate)
{
LazyTaskCompletedEventArgs args = pSate as LazyTaskCompletedEventArgs;
FireOnLazyTaskCompleted(args);
}
///
/// Lanza el evento de tarea completada
///
private void FireOnLazyTaskCompleted(LazyTaskCompletedEventArgs pArgs)
{
if (OnLazyTaskCompleted != null)
{
OnLazyTaskCompleted(this, pArgs);
}
}
///
/// Lanza el evento de progreso en la tarea
///
private void FireOnLazyTaskProgressChanged(LazyTaskProgressChangedEventArgs e)
{
if (OnLazyTaskProgressChanged != null)
{
OnLazyTaskProgressChanged(e);
}
}
Y para acabar con la implementación de delegados tan sólo nos falta implementar el delegado de finalización de la tarea. Este delegado se llamará cuando se haya terminado la ejecución de nuestra tarea. En este método es donde se elimina el identificador de la tarea de la colección y donde se da por terminada la tarea asíncrona llamando al método PostOperationCompleted. Una vez llamado este método, cualquier intento de utilización de la AsyncOperation a la que se refiere nos dará una excepción.
/// Método que se llamará cuando la ejecución de la tarea de forma asíncrona haya acabado
///
private void LazyTaskCompletionMethod(object pLazyTaskState)
{
LazyTaskState taskState = pLazyTaskState as LazyTaskState;
AsyncOperation operation = taskState.Operation;
int iNumIterations = taskState.NumIterations;
int iTotalTime = taskState.TotalTime;
bool bCancelled = false;
Exception eException = taskState.TaskException;
LazyTaskCompletedEventArgs args = new LazyTaskCompletedEventArgs(iNumIterations, iTotalTime, eException, bCancelled, operation.UserSuppliedState);
lock (m_ldTasks.SyncRoot)
{
if (m_ldTasks.Contains(operation.UserSuppliedState))
{
m_ldTasks.Remove(operation.UserSuppliedState);
}
}
operation.PostOperationCompleted(onCompletedDelegate, args);
}
Ahora ya podemos pasar a implementar la función que hará el trabajo real de nuestra clase. En nuestro caso será algo tan trivial como un for con un sleep dentro para simular una tarea pesada.
///
/// Realiza la operación que queremos
///
private void DoTask(AsyncOperation pOperation, SendOrPostCallback pCompletionMethod)
{
Exception ex = null;
try
{
for (int i = 0; i < 100; i++)
{ if (!TaskCanceled(pOperation.UserSuppliedState))
{
System.Threading.Thread.Sleep(100);
LazyTaskProgressChangedEventArgs pChangedEventArgs = new LazyTaskProgressChangedEventArgs(i + 1, pOperation.UserSuppliedState);
pOperation.Post(this.onProgressDelegate, pChangedEventArgs);
}
}
}
catch ( Exception e )
{
ex = e;
}
if (!TaskCanceled(pOperation.UserSuppliedState))
{
LazyTaskState state = new LazyTaskState(100, 10000, pOperation, ex);
pCompletionMethod(state);
}
}
En este código podemos ver tres cosas. La primera es que nos apoyamos en una función llamada TaskCancelled. Esta función, que la debemos implementar nosotros, simplemente mira en la colección si está el identificador de tarea que le pasamos como parámetro. Por otra parte podemos ver que cada vez que queremos informar de un cambio en el progreso de la tarea llamamos a la función Post de la AsyncOperation que recibimos como parámetro. Y finalmente vemos que cuando acabamos la realización de la tarea llamamos al delegado del completionMethod.
Y ya nos vamos acercando al final. Ahora tan sólo nos queda implementar los métodos para iniciar y cancelar el trabajo. En el método para iniciar la tarea nos tendremos que asegurar que el identificador de tarea que nos pasan cómo parámetro es único mirando si está en la colección. En el método para cancelar, tan sólo tendremos que quitar la tarea de la colección. El framework hará el resto.
///
/// Realiza la tarea asíncronamente
///
public virtual void DoLazyTaskAsync(object pTaskID)
{
AsyncOperation operation = AsyncOperationManager.CreateOperation(pTaskID);
lock (m_ldTasks.SyncRoot)
{
if (m_ldTasks.Contains(pTaskID))
{
throw new ArgumentException("Ya existe una tarea con este identificador", "taskID");
}
m_ldTasks[pTaskID] = operation;
}
doTaskDelegate = new WorkerEventHandler(DoTask);
doTaskDelegate.BeginInvoke(operation, completionMethodDelgate, null, null);
}
///
/// Cancela la ejecución de una tarea asíncrona
///
public void CancelLazyTaskAsync(object pTaskId)
{
lock (m_ldTasks.SyncRoot)
{
if ( m_ldTasks.Contains(pTaskId ) )
{
AsyncOperation operation = (AsyncOperation)m_ldTasks[pTaskId];
if (operation != null)
{
m_ldTasks.Remove(pTaskId);
}
}
}
}
Ya tenemos nuestra clase con una llamada a un método asíncrono preparada para ser utilizada por un cliente. Para completar su funcionalidad deberíamos tener también un método para que podamos llamarla de forma síncrona por si nos interesa.
La implementación del cliente es muy sencilla. Simplemente nos tenemos que subscribir a los eventos de progreso y finalización y llamar a los métodos de inicio y cancelación cuando nos interese. Podéis ver el código completo en el adjunto.
Espero que este pequeño ejemplo os haya servido de ayuda. Podéis descargaros el ejemplo de aquí.