El control BackgroundWorker
Una de las dificultades en la programación en capas es la de mostrar elementos en la interfaz gráfica de la aplicación mientras se realiza un proceso que lleva mucho tiempo en la capa de datos. Por ejemplo la clásica aplicación que muestra una barra de progreso de la tarea que se está ejecutando dentro de una clase que obtiene una gran información de una base de datos o que procesa una gran cantidad de datos. Cuando colocaba toda la funcionalidad dentro del formulario no era problema, pero ¿Cómo hacerlo ahora que mi clase está separada del formulario?. Primero lo resolví con un evento dentro de la clase que procesa la gran cantidad de información. Este evento se dispararía cada vez que quisiera que la barra de progreso se actualizara, pero me quedaba el problema de poder cancelar el proceso antes de que terminara. Entonces pensé en hilos (threads). Es complicado trabajar con hilos de ejecución, pero afortunadamente encontré un control que cubre esta funcionalidad en visual studio y se llama BackgroundWorker.El control BackgroudWorker permite ejecutar una operación en un subproceso (hilo) dedicado e independiente. La figura 1 muestra el ícono del control.
Figura 1 Icono del backgroundWorker
El control BackgroundWorker no es visible al usuario por lo que al agregarse se coloca en la barra debajo del formulario, donde se colocan otros controles como el timer por ejemplo.
Los eventos que maneja el control BackGroundWorker son DoWork donde se coloca el código del proceso que se va a ejecutar, el ProgressChanged que sirve para actualizar los controles de avance, por ejemplo un progressbar y el RunWorkCompleted que es el evento que indica el final del proceso.
Para ejecutar el subproceso se utiliza el método RunWorkerAsync del control, que puede recibir cualquier objeto como argumento. Es importante definir este objeto con todos los parámetros necesarios para el método que va a realizar el proceso dentro de DoWork.
El método CancelAsync sirve para cancelar el proceso, siempre y cuando la propiedad WorkerSupportCancellation del control BackgroundWorker este en true. El objeto que realiza el proceso debe revisar la propiedad CancellationPending del objeto BackgroundWorker y poner a true la propiedad Cancel del objeto de argumentos del evento para que se realice la cancelación.
El siguiente código muestra un ejemplo de como usar el control BackgroundWorker:
Primero defino la clase clsParamsProceso que me servirá para pasar todos los parámetros que requiero para realizar el proceso. A continuación escribo el código que llevará cada manejador de eventos del BackgroudWorker.
public class clsParamsProceso
{
private string _NombreArchivo;
public string NombreArchivo
{
get { return _NombreArchivo; }
set { _NombreArchivo = value; }
}
private int _Nivel1;
public int Nivel1
{
get { return _Nivel1; }
set { _Nivel1 = value; }
}
private int _Nivel2;
public int Nivel2
{
get { return _Nivel2; }
set { _Nivel2 = value; }
}
private int _Nivel3;
public int Nivel3
{
get { return _Nivel3; }
set { _Nivel3 = value; }
}
private int _Nivel4;
public int Nivel4
{
get { return _Nivel4; }
set { _Nivel4 = value; }
}
private string _CuentaBanco;
public string CuentaBanco
{
get { return _CuentaBanco; }
set { _CuentaBanco = value; }
}
private int _FilaInicio;
public int FilaInicio
{
get { return _FilaInicio; }
set { _FilaInicio = value; }
}
private string _ArchivoRangos;
public string ArchivoRangos
{
get { return _ArchivoRangos; }
set { _ArchivoRangos = value; }
}
}
private void btnProcCatalog_Click(object sender, EventArgs e)
{
clsParamsProceso parametros = new clsParamsProceso();
if (txtNomArchivoCat.Text != "")
{
if (!bgwProgreso.IsBusy)
{
tssEstado.Text = "Convirtiendo Catálogo...";
parametros.NombreArchivo = opdlgPricnipal.FileName;
parametros.Nivel1 = Convert.ToInt32(txtLonNivel1.Text);
parametros.Nivel2 = Convert.ToInt32(txtLonNivel2.Text);
parametros.Nivel3 = Convert.ToInt32(txtLonNivel3.Text);
parametros.Nivel4 = Convert.ToInt32(txtLonNivel4.Text);
parametros.CuentaBanco = txtCuentaBanco.Text;
parametros.FilaInicio = Convert.ToInt32(txtInicio.Text);
bgwProgreso.RunWorkerAsync(parametros);
HabilitarControles(false);
}
}
else
MessageBox.Show("No a seleccionado ningún archivo a convertir", "Error captura de datos", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
private void bgwProgreso_DoWork(object sender, DoWorkEventArgs e)
{
ctr.GenerarCatalogo(bgwProgreso, e);
}
private void bgwProgreso_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
prbPrincipal.Value = e.ProgressPercentage;
}
private void bgwProgreso_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
prbPrincipal.Value = 0;
HabilitarControles(true);
if (e.Error != null)
{
tssEstado.Text = "Error: " + e.Error.Message;
}
else if (e.Cancelled)
{
tssEstado.Text = "Conversión de catálogo Cancelada";
}
else
{
tssEstado.Text = "Conversión de catálogo finalizada exitósamente";
}
}
{
private string _NombreArchivo;
public string NombreArchivo
{
get { return _NombreArchivo; }
set { _NombreArchivo = value; }
}
private int _Nivel1;
public int Nivel1
{
get { return _Nivel1; }
set { _Nivel1 = value; }
}
private int _Nivel2;
public int Nivel2
{
get { return _Nivel2; }
set { _Nivel2 = value; }
}
private int _Nivel3;
public int Nivel3
{
get { return _Nivel3; }
set { _Nivel3 = value; }
}
private int _Nivel4;
public int Nivel4
{
get { return _Nivel4; }
set { _Nivel4 = value; }
}
private string _CuentaBanco;
public string CuentaBanco
{
get { return _CuentaBanco; }
set { _CuentaBanco = value; }
}
private int _FilaInicio;
public int FilaInicio
{
get { return _FilaInicio; }
set { _FilaInicio = value; }
}
private string _ArchivoRangos;
public string ArchivoRangos
{
get { return _ArchivoRangos; }
set { _ArchivoRangos = value; }
}
}
private void btnProcCatalog_Click(object sender, EventArgs e)
{
clsParamsProceso parametros = new clsParamsProceso();
if (txtNomArchivoCat.Text != "")
{
if (!bgwProgreso.IsBusy)
{
tssEstado.Text = "Convirtiendo Catálogo...";
parametros.NombreArchivo = opdlgPricnipal.FileName;
parametros.Nivel1 = Convert.ToInt32(txtLonNivel1.Text);
parametros.Nivel2 = Convert.ToInt32(txtLonNivel2.Text);
parametros.Nivel3 = Convert.ToInt32(txtLonNivel3.Text);
parametros.Nivel4 = Convert.ToInt32(txtLonNivel4.Text);
parametros.CuentaBanco = txtCuentaBanco.Text;
parametros.FilaInicio = Convert.ToInt32(txtInicio.Text);
bgwProgreso.RunWorkerAsync(parametros);
HabilitarControles(false);
}
}
else
MessageBox.Show("No a seleccionado ningún archivo a convertir", "Error captura de datos", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
private void bgwProgreso_DoWork(object sender, DoWorkEventArgs e)
{
ctr.GenerarCatalogo(bgwProgreso, e);
}
private void bgwProgreso_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
prbPrincipal.Value = e.ProgressPercentage;
}
private void bgwProgreso_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
prbPrincipal.Value = 0;
HabilitarControles(true);
if (e.Error != null)
{
tssEstado.Text = "Error: " + e.Error.Message;
}
else if (e.Cancelled)
{
tssEstado.Text = "Conversión de catálogo Cancelada";
}
else
{
tssEstado.Text = "Conversión de catálogo finalizada exitósamente";
}
}
Finalmente, dentro de mi clase control, el metodo GenerarCatalogo quedaría de la siguiente forma:
public int GenerarCatalogo(BackgroundWorker trabajador,
DoWorkEventArgs e)
{
int Avance = 0;
for (int i = 0; i < 10000; i++)
{
//Lineas de codigo del proceso....
Avance = (int)(i * 100.0 / 10000);
trabajador.ReportProgress(Avance);
if (trabajador.CancellationPending)
{
e.Cancel = true;
break;
}
}
}
DoWorkEventArgs e)
{
int Avance = 0;
for (int i = 0; i < 10000; i++)
{
//Lineas de codigo del proceso....
Avance = (int)(i * 100.0 / 10000);
trabajador.ReportProgress(Avance);
if (trabajador.CancellationPending)
{
e.Cancel = true;
break;
}
}
}
Cada ciclo calculo el avance y se lo envío al método ReportProgress del objeto BackgroundWorker
Espero que este control les sea de utilidad y comiencen a experimentar con hilos de procesos, un tema bastante interesante, sobre todo en la ejecución en paralelo.