[HowTo] Crear una pantalla que espere la conclusión de una tarea larga

WaitForTaskScreen

Nota: Otro post en respuesta a una pregunta bastante habitual en los foros MSDN: “Mi frmPrincipal ejecuta una función (no tengo el tiempo preciso) y mientras realiza esto quiero que aparezca una ventana … que impida el acceso al frmPrincipal y una vez que se termine de ejecutar la función, desaparezca frmMensaje.”

La verdad es que hay un montón de posibles soluciones. Desde el uso del componente BackgroundWorker (la opción más sencilla pero la que menos me gusta :-P) hasta los futuros async y await que vendrán con C# 5, pasando por los clásicos threads de toda la vida o las tareas que se introdujeron con la TPL en Visual Studio 2010.

Para este ejemplo vamos a utilizar ésta última aproximación, ya que de las otras hay millones de ejemplos de todos los tipos y colores. Además, puede servir como -sencillo- ejercicio de uso de la clase Task, para que quién no la conozca vaya introduciéndose en el maravilloso mundo de la programación paralela.

Primeramente vamos a crear un formulario con un título, un mensaje y una barra de progreso de tipo marquesina, ya que como dice el enunciado “no se conoce de antemano la duración de la tarea a ejecutar”. Y para comprobar que la tarea se ejecuta en segundo plano (en realidad en paralelo si hay más de un core en el procesador) también añadiremos un botón para poder cerrar la ventana.

Aviso: Este botón únicamente sirve para cerrar la ventana y retornar a la ventana principal.  Realmente no va a cancelar la tarea, para eso se requeriría un CancellationToken, y no quiero complicar tanto el ejemplo... Aunque tal vez sea buena idea verlo en otro post, más adelante.

El punto de partida será la ventana principal. En mi caso y para no complicarme la vida he creado un formulario muy sencillo con un sólo botón, y que contiene un método que se supone que va a tardar un cierto tiempo en ejecutarse. Iba a poner ‘un huevo’ pero prefiero no hacerlo para no herir susceptibilidades :-)

El formulario principal:

MainForm

El método que tarda… mucho:

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

El formulario que espera a que la tarea termine:

WaitForTaskScreenDesign

Una vez diseñada el formulario de espera, le agregaremos algunas propiedades como el título, mensaje, y sobre todo el método que deseamos ejecutar en forma de Action:

public string Title { get; set; }
public string Message { get; set; }
public Image Picture { get; set; }
public Action ActionToExecute { get; set; }

Una vez creadas estas propiedades, podemos volver al formulario principal y hacer la llamada a la ventana de espera, pasándole los valores oportunos:

private void button1_Click(object sender, EventArgs e)
{
    using (fWaitForTaskScreen fwait = new fWaitForTaskScreen())
    {
        fwait.Title = "Executing a looooong task...";
        fwait.Message = "This form will close when the task completes its work.\nJust wait a few seconds... :-)";
        fwait.Picture = Properties.Resources.LogoWeb30_small;
        fwait.ActionToExecute = DoSomeHardWork;
        if (fwait.ShowDialog() == DialogResult.OK)
        {
            MessageBox.Show("OK");
        }
        else
        {
            MessageBox.Show("Cancel or error");
        }
    }   
}

Lo único importante aquí es la forma en que le pasamos el método que debe ejecutar.  Estamos usando una propiedad de tipo Action, que representa un método con un parámetro que no retorna un valor. De modo, que -ya os aviso- sirve para ejecutar cualquier método que no devuelva un valor. En caso que queramos devolver un valor podríamos utilizar una propiedad pública, o mejor un delegado Func<T, TResult>. Pero como os decía antes, prefiero no complicar el ejemplo.

El código es muy sencillo, como podéis ver. Crea el formulario de espera, le pasa los valores (incluyendo el método a ejecutar) y examinamos el valor de DialogResult para saber si ha terminado con éxito la tarea o no, o se ha cancelado por el usuario.

Y a todo esto ¿cómo se ejecuta el método de forma asíncrona?

Pues aquí está la gracia de este ejercicio. Ya veréis lo sencillo que resulta, sobre todo para aquellos acostumbrados a trabajar ‘a pelo’ con threads:

void fSplashScreen_Shown(object sender, EventArgs e)
{
    titleLabel.Text = Title;
    messageLabel.Text = Message;
    pictureBox.Image = Picture;
    Task.Factory.StartNew(ActionToExecute).ContinueWith((t) => taskCompleted());
}
 
private void taskCompleted()
{
    if (InvokeRequired)
    {
        this.Invoke((MethodInvoker)(() =>
        {
            Close();
            DialogResult = DialogResult.OK;
        }));
    }
}

Vayamos por partes. El primer bloque es realmente la parte importante. De hecho, la última línea es la que se encarga de todo. Crea y ejecuta una tarea asíncrona que apunte al método que le hemos pasado al formulario, y no sólo hace eso, si no que además le decimos que cuando termine ejecute una segunda tarea ‘taskCoompleted’. A esto se le llama ‘encadenamiento’ de tareas, y es muy útil cuando una tarea depende de otra.

Y que hace la segunda tarea? Apenas nada. Cierra el formulario y devuelve el valor OK para que la ventana principal sepa que todo ha terminado bien. Lo único que sucede es que -al igual que cuando usamos threads- en .NET no se puede modificar un control fuera del thread que lo ha creado, eso es, el thread principal.  Y nuestras tareas se están ejecutando en otros threads creados de forma transparente por la tarea. De modo que para poder llamar al método Close o modificar la propiedad DialogResult debemos hacerlo mediante Control.Invoke, y por ese motivo el código es un poco extraño a primera vista.

WaitForTaskScreenFinal

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

Nos vemos,

Published 28/9/2011 15:49 por Lluis Franco
Comparte este post:

Comentarios

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

Wednesday, September 28, 2011 5:58 PM por Lluis Franco

Por cierto, si hay suerte mañana ampliaremos el post con una segunda versión gracias a una ayuda de Pedro Hurtado en los foros MSDN :-)