[HowTo] Crear una pantalla que espere la conclusión de una tarea larga (con cancelación)

WaitForTaskScreen

En el post anterior vimos un ejemplo de cómo crear una ventana que espere la conclusión de una tarea. Si bien este ejemplo cubría sobradamente el objetivo, dejamos abierta la puerta a la posibilidad de que dicha tarea pudiese ser cancelada por petición del usuario, pulsando el botón ‘cancel’ de la ventana.

Si se hizo de este modo fue para evitar complicar en exceso el post, ya que el objetivo inicial era simplemente “mostrar una ventana mientras durase el proceso”.

Tras publicar el post y ver algunas de las reacciones en el hilo del foro de C# de MSDN (gracias Pedro por tu idea), he decidido ampliar el proyecto de ejemplo ofreciendo al usuario la posibilidad de cancelar REALMENTE la tarea, utilizando para ello un CancellationToken.

CancellationToken es una estructura nueva del .NET Framework 4.0 (así como toda la TPL en sí), y permite la cancelación de un proceso asíncrono. Es comúnmente usada cuando deseamos propagar una notificación de cancelación hacia arriba, por ejemplo, hacia el método que está siendo ejecutado como en nuestro caso.

Para ello necesitamos crear previamente un CancellationTokenSource, que es el objeto que contiene el token de cancelación y que además nos va a permitir cancelar la tarea. Posteriormente este objeto nos proporcionará el token de cancelación, que usaremos en la llamada a la tarea. Pero vamos a verlo mejor con un poco de código:

En el formulario de espera:

Aparecen algunos cambios, por ejemplo debemos modificar la firma de la propiedad ActionToExecute para que acepte un método que contenga un parámetro de tipo CancellationToken. Este parámetro será usado por el método para saber si se ha cancelado y actuar en consecuencia. De paso aprovecharemos para declarar el CancellationTokenSource.

public Action<CancellationToken> ActionToExecute { get; set; }
CancellationTokenSource cancelTokenSource = new CancellationTokenSource();

Nota importante: En el proyecto anterior, el botón ‘cancel’ no tenía código asociado. Simplemente tenía establecida la propiedad DialogResult en Cancel, de modo que al pulsarlo cerraba automáticamente la ventana y devolvía el valor Cancel a su llamador. En el proyecto actual como sí que nos interesa controlar el cierre de la ventana, es necesario establecer la propiedad DialogResult en None e interceptar su evento:

closeButton.Click += closeButton_Click;
 
void closeButton_Click(object sender, EventArgs e)
{
    //
}

Aquí más adelante cancelaremos la tarea y devolveremos un valor DialogResult = Cancel.

Ahora vamos a centrarnos en la creación y ejecución de la tarea asíncrona:

void fSplashScreen_Shown(object sender, EventArgs e)
{
    titleLabel.Text = Title;
    messageLabel.Text = Message;
    pictureBox.Image = Picture;
    Task.Factory.StartNew(() => 
        ActionToExecute(cancelTokenSource.Token)).ContinueWith((t) => taskCompleted());
}

Como veis en realidad no hay demasiados cambios, lo único es que en la llamada a StartNew obtenemos el token de cancelación de su CancellationTokenSource, y cuando posteriormente ésta sea cancelada por el usuario, el token será propagado hasta el método que se está ejecutando, permitiendo su cancelación.

Hablando del método, que modificaciones hay que realizar?

El método original era este:

private void DoSomeHardWork()
{
    for (int i = 0; i < 50; i++)
    {
        Console.WriteLine(i);
        Thread.Sleep(100);
    }            
}

Y el nuevo quedaría así:

private void DoSomeHardWorkWithCancellation(CancellationToken cancelToken)
{
    try
    {
        for (int i = 0; i < 50; i++)
        {
            cancelToken.ThrowIfCancellationRequested();
            Console.WriteLine(i);
            Thread.Sleep(100);
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, Application.ProductName, 
            MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    }
}

Como decíamos antes, este método recibe un parámetro de tipo CancellationToken y lo único que hay que hacer es lanzar una excepción en caso que haya sigo solicitada una cancelación (de ahí que se envuelva el código dentro de un bloque try-catch).

Nada más. Una sólo línea y el pertinente bloque de intercepción de errores… y voilá! No os parece bonito y elegante? :-)

Por cierto, en caso que no se desee lanzar una excepción es posible que os interese preguntar por el valor de la propiedad IsCancellationRequested y elegir vosotros mismos la acción más apropiada.

Volvamos al formulario de espera y completemos el código que falta. Primeramente el código del botón ‘cancel’, encargado de cancelar realmente la tarea:

void closeButton_Click(object sender, EventArgs e)
{
    DialogResult = DialogResult.Cancel;
    cancelTokenSource.Cancel();
    releaseCancellationTokenSource();
}
 
private void releaseCancellationTokenSource()
{
    if (cancelTokenSource != null)
    {
        cancelTokenSource.Dispose();
        cancelTokenSource = null;
    }
}

Y por último, agregar una línea para liberar también el token al finalizar con éxito la tarea:

private void taskCompleted()
{
    if (InvokeRequired)
    {
        this.Invoke((MethodInvoker)(() =>
        {
            Close();
            DialogResult = DialogResult.OK;
        }));
    }
    releaseCancellationTokenSource();
}

Y con esto y un bizcocho, hemos terminado! Espero que os haya gustado :-D

Muchas gracias de nuevo a Pedro Hurtado y a toda la gente del foro de C# en MSDN :-)

Os dejo también el mini proyecto de ejemplo por si queréis jugar un rato:

Nos vemos,

Published 29/9/2011 11:52 por Lluis Franco
Comparte este post:

Comentarios

# re: [HowTo] Crear una pantalla que espere la conclusión de una tarea larga (con cancelación)

Thursday, September 29, 2011 3:59 PM por Kiquenet

Interesantes artículos. Les felicito.

Del tema de la ejecución de tareas de gran duración (long tasks) dos apuntes a tener en cuenta:

- Cómo realizar una cancelación de tareas de gran duración como acceso a bases de datos Sql Server u Oracle, llamadas a servicios web-wcf, procesos con ficheros (File.Copy,...)

- Considerar un tiempo límite de ejecución (timeouts) para ejecutar la tarea, y si se supera ese tiempo hacer el tratamiento adecuado. No vi en foros algún ejemplo clarificador al respecto.

- Aspectos más avanzados que comentan pero se escapa a mi nivel: sistemas de colas de peticiones para ejecución asíncrona de peticiones.

Saludos y gracias.

# re: [HowTo] Crear una pantalla que espere la conclusión de una tarea larga (con cancelación)

Thursday, September 29, 2011 6:15 PM por El Bruno

Que buen post LF, te lo has currado thanks !!! ya me he inspirado en alguna que otra idea de por aqui :D

Saludos

# re: [HowTo] Crear una pantalla que espere la conclusión de una tarea larga (con cancelación)

Friday, September 30, 2011 10:49 AM por Lluis Franco

@kiquenet: Gracias ;-) Respecto a las tareas de larga duración que mencionas... es complicado. Más que nada porque quién gestiona la tarea no es nuestra aplicación, de modo que lo mejor en estos casos es segmentar la tarea en varias pequeñas y comprobar si se ha cancelado antes e ejecutar cada una de las partes.

@bruno: Como si a tí te hiciesen falta ideas... :-P