Backgroundworker. Mejorando la interfaz de usuario en aplicaciones .NET

Cuando uno se para a pensar y crear el diseño de una aplicación hay muchas preguntas con las que se suele comenzar. ¿La interfaz es clara?, ¿El usuario sabrá que “X” botón sirve para realizar “Y” acción?, ¿Es atractiva la interfaz?.

Desde los comienzos de cualquier programador dichas preguntas están en su mente pero hay otras igual o más importantes que al comienzo se suelen escapar.  Preguntas como … ¿Estoy consumiendo recursos en exceso?, ¿Podría hacer “X” rutina de una forma más óptima?.

Imagínate que tenemos una aplicación que trabaja con acceso a base de datos. Si deseamos obtener un informe que realice una consulta SQL para obtener por ejemplo las ventas realizadas durante un año completo, dicha consulta puede llegar a tardar “bastante”. Si toda la lógica a realizar se produce al hacer clic sobre un botón, podemos llegar a “congelar” la aplicación hasta que toda la lógica haya finalizado.

¿Por qué ocurre esto?

Es sencillo. Absolutamente todo lo estamos ejecutando en un mismo hilo.

La pregunta siguiente es obvia. ¿Y qué es un hilo (Thread)?

Un hilo no es más que una tarea que puede llegar a ejecutarse en paralelo. En .NET, cuando se lanza una aplicación se crea un proceso y dentro de este proceso un hilo de ejecución(Thread) para el método Main (Hilo Principal).

En Windows, se permite ejecutar varios hilos simultáneamente aun cuando sólo se tenga un único procesador. Lo que se hace es ofrecer un tiempo determinado de ejecución a cada hilo. Cuando ese tiempo termina, Windows retoma el control y se lo cede a otro hilo diferente.

Retomando el ejemplo anterior. Lo ideal sería que la aplicación con su interfaz de usuario se ejecutara en el Hilo Principal. Al pulsar el botón toda la lógica de acceso a la base de datos se crea en un hilo aparte mostrando al cliente algún tipo de información (típica barra de progreso o mensaje informativo). Esto permite no congelar la aplicación pudiendo mostrar información sin dejar al usuario con una mala sensación ó incluso le permitimos seguir utilizando otros apartados de la aplicación sin problemas.

¿Cómo hacemos eso?, ¿Cómo creamos otros hilos para hacer tareas pesadas?

Realmente el objetivo propuesto se puede llegar a conseguir de múltiples formas (Una de ellas diferente a la que veremos sería utilizando System.Threading). Sin embargo, la mejor de todas las opciones posibles considero que es utilizando la clase Backgroundworker.

Backgroundworker

La clase Backgroundworker está disponible desde el Framework 2.0 y nos permite crear hilos diferentes del hilo donde se ejecuta la interfaz de usuario para lograr realizar tareas costosas sin afectar a la misma. Para utilizar la clase Backgroundworker hacemos uso de la librería System.dll y necesitamos el namespace System.ComponentModel.

La principal ventaja de esta forma de crear hilos frente a otras es que toda la gestión de los hilos queda encapsulada en el propio control.

Veamos como realizar todo eso con un ejemplo. El ejemplo está realizado en WPF. Mostramos el XAML a continuación:

<Window x:Class="Ejemplo_BackgroundWorker_1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Ejemplo 1 BackgroundWorker" Height="300" Width="300">
<Grid>
 <Button HorizontalAlignment="Left" Margin="8,25,0,0" x:Name="_btnStart" VerticalAlignment="Top" Width="87" Height="27" Content="Start" Click="StartWorker"/>
 <Button IsEnabled="False" Margin="99,25,102,0" x:Name="_btnCancel" VerticalAlignment="Top" Height="27" Content="Cancel" Click="CancelWorker"/>
 <Label Margin="78,103,83,131" Name="lbPercentage" Content="" />
 <ProgressBar Margin="8,0,8,107" x:Name="_progressBar" VerticalAlignment="Bottom" Height="23" Maximum="10" />
 <TextBox Height="23" Margin="55,0,12,49" Name="txtInfo" VerticalAlignment="Bottom" BorderBrush="DarkGray" />
 <Label Height="28" HorizontalAlignment="Left" Margin="0,0,0,49" Name="lbInfo" VerticalAlignment="Bottom" Width="55">Escriba:</Label>
</Grid>
</Window>

Básicamente tenemos dos botones. Uno para comenzar un BackgroundWorker y otro para detenerlo. Una barra de progreso que comenzará en cuanto se pulse el botón “Start” y un textbox donde escribir. Visualmente algo similar a lo siguiente:

En esta entrada se colgarán dos ejemplos que podrás descargar al final de la misma. El primero de ellos hace uso de BackgroundWorker y el segundo no.

¿Por qué dos ejemplos?

Para ver de forma rápida t efectiva las ventas de usar hilos. En el primer de ellos al pulsar el botón “Start” podremos escribir lo que queramos en el textbox mientras la barra de progreso continua. Sin embargo, en el segundo al pulsar sobre el botón “Start” perderemos el control sobre la interfaz no pudiendo escribir absolutamente nada.

Vamos a ver y entender el código que realiza toda la lógica en nuestro ejemplo:

 private BackgroundWorker _worker;
 int percentage = 0;

 public Window1()
 {
InitializeComponent();
 }

 private void StartWorker(object sender, RoutedEventArgs e)
 {
_worker = new BackgroundWorker();
_worker.WorkerReportsProgress = true;
_worker.WorkerSupportsCancellation = true;

_worker.DoWork += delegate(object s, DoWorkEventArgs args)
{
BackgroundWorker worker = s as BackgroundWorker;
for (int i = 0; i < 10; i++)
{
if (worker.CancellationPending)
{
args.Cancel = true;
return;
}

Thread.Sleep(1000);
worker.ReportProgress(i + 1);
}
};

_worker.ProgressChanged += delegate(object s, ProgressChangedEventArgs args)
{
percentage = args.ProgressPercentage;
_progressBar.Value = args.ProgressPercentage;
lbPercentage.Content = (percentage * 10) + "% Completado";
};

_worker.RunWorkerCompleted += delegate(object s, RunWorkerCompletedEventArgs args)
{
_btnStart.IsEnabled = true;
_btnCancel.IsEnabled = false;
_progressBar.Value = 0;
};

_worker.RunWorkerAsync();
_btnStart.IsEnabled = false;
_btnCancel.IsEnabled = true;
 }

 private void CancelWorker(object sender, RoutedEventArgs e)
 {
_worker.CancelAsync();
 }

El ejemplo es supersencillo pero lo suficientemente completo como para cubir practicamente todas las posibilidades del BackgroundWorker. Como cancelar el hilo, como notificar del progreso realizado, etc.

Aquello que no aparece en el ejemplo (el objetivo del mismo era ser lo más simple posible) como gestión de errores ó que se puede y no hacer dentro de un BackgroundWorker también lo veremos.

El método RunWorkerAsync() es el encargado de desencadenar todo el proceso.  Este método invoca al DoWork donde se realizan todos aquellos procesos que queramos en un hilo aparte. Todo lo que se ejecuta en el DoWork lo hace en un hilo diferente al de la interfaz por lo tanto NO intentes acceder directamente a cualquier elemento visual de la misma dentro del DoWork. Entonces . . . ¿No podemos acceder a la interfaz de usuario desde el DoWork?

No, no es así. Realmente si que podemos pero neceistaríamos utilizar para ello un delegado. Sería algo asi:

System.Windows.Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (DispatcherOperationCallback)delegate(object arg)
{
string prueba = lbPercentage.Content;
return null;
}, null);

Como ves se intenta obtener el contenido de un label de la interfaz. El Dispatcher (en otra futura entrada veremos esto mucho mejor) nos ejecuta esa línea de código desde el hilo principal. Es decir, el mismo hilo de la interfaz de usuario.

Otro punto importante que aunque no es obligatorio es muy recomendado sería no utilizar try-cath dentro del DoWork. ¿Por qué?

EL BackgroundWorker gestiona de forma automática los errores. Tanto si finaliza toda la lógica dentro del DoWork como si ocurre una excepción ó se cancela la ejecución del BackgroundWorker se llamará al RunWorkerCompleted.

El objeto RunWorkerCompletedEventArgs no porporcionará la información necesaria para saber si el DoWork finalizó correctamente, con error o se canceló. Veamos como sería:

private void _worker_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
 if (e.Cancelled)
 {
 //Ha sido cancelado.
 }
 else if (e.Error != null)
 {
 //Ha ocurrido algun tipo de excepción no controlada.
 }
 else
 {
 //Todo finalizó correctamente.
 }
}

¿Y cómo se gestiona la barra de progreso?

Para ese tipo de gestión utilizamos el método ProgressChanged. Todo lo que se ejecute en este método lo hará en el hilo principal por lo que podemos acceder directamente a la interfaz de usuario. El objeto ProgressChangedEventArs nos proporciona toda la información básica necesaria para saber el estado relacionado con el progreso en el que nos encontramos en todo momento. Como ya vimos anteriormente en el código:


_progressBar.Value = args.ProgressPercentage;

Si la propiedad WorkerSupportsCancellation tiene como valor true, podremos cancelar la ejecución del hilo en todo momento. Bastaría con hacer lo siguiente:

backgroundWorker1.CancelAsync();

Por último, os dejo los enlaces a cada unos de los dos ejemplos que ya hemos comentado con anterioridad. Los podéis encontrar aquí y aquí. El primer de ellos hace uso del BackgroundWorker mientras que el segundo no.

2 pensamientos en “Backgroundworker. Mejorando la interfaz de usuario en aplicaciones .NET

  1. Interesante la teoria pero si lo que se necesita es lanzar una consulta a una base de datos, como se sabe que porcentaje ir avanzando a la progressbar?. En tu codigo tienes un for (int i = 0; i < 10; i++).

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s