.NET Tutorial 51. Aplicaciones con hilos. Uso práctico de BackgroundWorker

No hace mucho Btc me comentó algunos problemas que tenía con su B-File Renamer, en concreto con la gestión de hilos que hace su programa para el renombrado masivo de los archivos. Por este motivo me he animado a escribir este pequeño tutorial para mostrar un pequeño ejemplo práctico de como implementar una ‘solución’ que emplea el uso de hilos (threading en inglés).

Una de las principales ventajas de las aplicaciones que emplean hilos es que pueden hacer ‘más de una cosa’ al mismo tiempo. En la realidad prácticamente todas las aplicaciones ya usan internamente más de un hilo, lo que ocurre es que esto se realiza de forma transparente para nosotros.

En nuestro caso queremos que un determinado método, función, clase o lo que sea se ejecute en otro hilo. Normalmente estos métodos, funciones, clases o lo que sea que queremos ejecutar dentro de un hilo consumen bastante tiempo.

Pongamos el caso del B-File Renamer. Renombrar todos los archivos de una determinada carpeta puede llevar 1 minuto o más, dependiendo lógicamente de la cantidad de archivos a renombrar.

Lo que no debería hacer la aplicación (B-File Renamer en este caso) es ‘quedarse colgada’ mientras renombra 2000 ficheros.

En este tipo de escenarios se puede implementar un BackgroundWorker, que es lo que veremos a continuación.

Para ello he simulado una aplicación que calcula la suma de todos los números comprendidos entre 1 y 1000

A esta suma se le ha añadido un retardo (Sleep) para ‘simular’ la carga de trabajo del método en cuestión.

La aplicación básicamente es esta:

Se calculará la suma sin usar hilos y usando un BackgroundWorker

Además se dispone de una barra de progreso que indica cuanto falta para finalizar todo el proceso y un botón de ‘stop’ que detiene el proceso.

Si se detiene el proceso se indica en la etiqueta correspondiente con el texto "Cancelado"

Si ejecutáis el programa, aparentemente tanto la opción ejecutar sin ‘Hilos’ como la opción usar ‘BackgroundWorker’ funcionan igual: muestran el resultado, la barra de progreso se va actualizando, el botón ‘stop’ funciona… sin embargo esto es solo apariencia. 🙂

Si ejecutáis el ejemplo sin usar hilos, intentar mover la ventana mientras se está ejecutando. Oh sorpresa! Observaréis que tanto el resultado como la barra de progreso ‘se han parado’.

Cuando dejéis de mover la ventana observaréis que tanto el resultado como la barra de progreso se vuelven a actualizar.

Pero hay más, si ahora pulsáis sobre el menú Archivo, al seleccionar cualquier elemento del menú se muestra un MessageBox. Al mostrarse el MessageBox se puede observar que tanto el resultado como la barra de progreso ‘se han parado’

Nada de esto ocurre cuando se usa un BackgroundWorker. Podéis mover la ventana de la aplicación o pulsar en cualquier item del menú y veréis que tanto el resultado como la barra de progreso se siguen ‘actualizando’

Del BackgroundWorker nos serán útiles 3 eventos:

  • DoWork
  • ProgressChanged
  • RunWorkerCompleted

DoWork

El evento DoWork se dispara cuando se llama al método RunWorkerAsync

En nuestro caso tendremos algo como esto:

Private Sub BackgroundWorker1_DoWork()
   e.Result = DoAlgo()
End Sub

DoAlgo() es la función que calcula la suma de los 1000 primeros números. Esta función devuelve el resultado en un dato de tipo Integer que se asigna a e.Result que es de tipo genérico (Object)

Cuando DoAlgo() haya calculado la suma de los 1000 primeros números, guardará el resultado de dicha suma en e.Result y se disparará inmediatamente el envento  RunWorkerCompleted()

Para evitar que se dispare el evento DoWork antes de que el BackgroundWorker haya finalizado la tarea se puede llamar a la función IsBusy:

If Not BackgroundWorker1.IsBusy Then
   BackgroundWorker1.RunWorkerAsync()
End If

ProgessChanged

Este evento se dispara cuando se llama al método ReportProgress

Dentro de nuestra función DoAlgo() vamos sumando los números. Al tratarse de un simple bucle For se puede computar fácilmente el porcentaje de números que llevamos sumados:

valorPorcentaje = Convert.ToInt32((i / Total) * 100)
BackgroundWorker1.ReportProgress(valorPorcentaje)

Dentro del evento ProgressChanged tenemos:

Private Sub BackgroundWorker1_ProgressChanged()
   Me.ProgressBar1.Value = e.ProgressPercentage
End Sub

Como se puede ver, lo único que se hace es actualizar el valor un control ProgresBar

Aquí cabe destacar que en este caso no es necesario utilizar ningún delegado para actualizar el contenido del control Progressbar.

Si intentáis cambiar el texto, la visibilidad, enable/disable, etc de un control (botón, etiqueta, caja de texto, listview, etc) desde dentro de un hilo distinto al hilo principal, deberéis usar delegados.

En nuestro ejemplo se muestra el uso de un delegado en la función MostrarResultado() que actualiza el texto de la etiqueta LblStatus desde dentro de DoAlgo() . DoAlgo() se está ejecutando en un hilo distintoal hilo principal del programa.

Otra opción es usar por ejemplo en el Form_Load() la siguiente instrucción: (MSDN)

Control.CheckForIllegalCrossThreadCalls = False  

Con eso podríamos usar LblStatus.Text = retSuma.ToString() desde dentro de DoAlgo() sin que se produjese ninguna excepción.

RunWorkerCompleted

Este evento se dispara cuando el método, función, etc que se ha iniciado en DoWork ha finalizado. También se dispara este evento cuando se cancela el proceso.

Para cancelar el proceso se llama al método CancelAsync

En nuestro caso tenemos un botón "stop" que hace lo siguiente:

BackgroundWorker1.CancelAsync()

Luego, dentro de nuestro DoAlgo() tenemos algo como esto:

For xxxx
   retSuma = realizar suma

   If BackgroundWorker1.CancellationPending Then
      retSuma = Nothing
      Exit For
   End If

Next

Return retSuma

Esto provoca que salga del bucle For y que el resultado que se guarda en e.Result sea Nothing.

Finalmente en el evento RunWorkerCompleted hacemos lo siguiente:

Private Sub BackgroundWorker1_RunWorkerCompleted()
   If e.Result <> Nothing Then
      LblStatus.Text = e.Result
   Else
      LblStatus.Text = "Cancelado"
   End If
End Sub

 

Pues poco más hay que añadir. Cómo habréis visto el uso de un BackgroundWorker es bastante sencillo.

En .NET Framework 4.0 se ha introducido un nuevo concepto para el manejo de ‘hilos’. Es lo que se conoce como TPL: Task Parallel Library (MSDN)
La TPL es un concepto interesantísimo al que seguramente dedicaremos alguna entrada.

 

Saludos.
mov eax,ollydbg; Int 13h

Descargar código fuente del .NET Tutorial 51
(23 KB. Visual Studio 2008)

 

.NET Tutorial 50. Base de datos (Parte III). Sistema ‘On-Line’ High-Scores con SQL Server

Ya hace algún tiempo vimos cómo trabajar con bases de datos (Tutorial 22 y Tutorial 23)Hoy veremos como usar una base de datos, SQL Server en este caso en una aplicación Web.

En esta base de datos que está alojada en un servidor Web enviaremos una serie de "puntuaciones" mediante una aplicación de Windows Forms que posteriormente se visualizarán.

Lo primero que deberemos hacer es crear tanto la base de datos como la tabla para las "puntuaciones".

Una forma rápida de hacer esto es usando el Microsoft SQL Server Management Studio del que ya hablamos en los tutoriales anteriores.

Para este tutorial se ha creado la base de datos: testASPdatabase
En dicha base de datos se ha creado una tabla llamada: HIGHSCORES

Sería algo como esto:

La tabla HIGHSCORES tiene únicamente tres campos:

  • autoID
  • UserName
  • Score

El campo autoID es un campo autonumérico (Especificación de Identidad: ) y es la clave principal de la tabla.

Bien, una vez creada la base de datos ya estamos en disposición para continuar. Este tutorial se divide en dos partes. Por un lado tendremos una aplicación "Web forms" que se encarga de realizar las operaciones sobre la base de datos y por otro lado tendremos una aplicación "Windows forms" que se encarga de "visualizar" la puntuación.

Aplicación Web Forms

Lo primero que tendremos que hacer para poder comunicarmos con nuestra base de datos de SQL server es definir la cadena de conexión en el fichero Web.config

<connectionStrings>
<
add name="connLocal" connectionString="Data Source=(local)SQLEXPRESS; initial catalog=testASPdatabase; Integrated Security=True;" providerName="System.Data.SqlClient"/>
</ connectionStrings>

Aquí vemos 4 cosas:

  • El parámetro de la cadena de conexión lo hemos llamado connLocal
  • El servidor está corriendo localmente en nuestra máquina y se llama SQLEXPRESS
  • La base de datos se llama testASPdatabase
  • La autentificación es autentificación de "Windows"

En el caso de utilizar una base de datos donde se requiera usuario y contraseña, que por otra parte es lo habitual en el 100% de los hostings de pago/gratuitos que hay en internet, la cadena de conexión sería así:

<add name="connHost" connectionString="DataSource=www.DataBaseHost.com; initial catalog=xxxx_DatabaseName_xxxx;user id=xxxx_DatabaseUser_xxxx; password=xxxx_DatabasePassword_xxxx" providerName="System.Data.SqlClient"/> 

 

La parte de la "aplicación Web" es tremendamente simple. Básicamente lo único que tenemos es un control "GridView" que mostrará los datos que recupera de la base de datos

Estos datos se guardan en la base de datos procedentes de un queryString.

La aplicación de "Windows forms", llama a a aplicación "Web Forms" de la siguiente forma:

http://localhost:49418/Default.aspx?HIscore=xxxxxxxxxx

Nota: En mi caso particular, mi localhost está corriendo en el puerto 49418. Si en tú caso está corriendo en otro puerto, establece el valor apropiado en el código de la aplicación "Windows Form"

La aplicación "Web Forms" analiza el valor de HIscore que se le ha pasado como argumento e inserta un nuevo registro en la base de datos. Una vez insertado dicho registro, actualiza el GridView, que es a la postre lo que se mostrará en la aplicación de Windows.

Como apunte final, el valor de HIscore=xxxxxxxxxx  se pasa "codificado". Por lo tanto, antes de insertar el registro en la base de datos hay que "descodificarlo" y obtener el "Nombre" y el valor de los "Puntos".

 

Aplicación Windows

La "aplicación Windows" simula de alguna forma un juego que has desarrollado en VB.NET / C#.

Al final del "juego" se envía la puntuación del jugador a un marcador "On-Line" y se muestra en que posición ha quedado.

Principalmente todo esto se hace gracias al control "WebBrowser".

Cuando se inicia el programa se llama al método Navigate:

WebBrowser1.Navigate("http://localhost:49418/")

Esto lo que hace es "cargar" el control WebBrowser con dicha dirección. Lo que se muestra es más o menos esto:


(Haz click para agrandar)

Eso que se muestra, no es más ni menos que la página "Default.aspx" de nuestra aplicación "Web forms"

Al introducir un nombre y puntuación y pulsar en el botón Enviar lo que se está haciendo es esto:

WebBrowser1.Navigate("http://localhost:49418/Default.aspx?HIscore=" & sQueryCifrado)

Esto provoca que la página "Default.aspx" inserte la puntuación en la base de datos y que refresque el GridView.

Por ejemplo si introducimos esto:

  • Nombre: Nefarian
  • Score: 35000

Y pulsamos en el botón Enviar se observa lo siguiente:


(Haz click para agrandar)

Si ahora introducimos esto:

  • Nombre: Ollydbg
  • Score: 125

y pulsamos en el botón Enviar obtenemos lo siguiente:


(Haz click para agrandar)

Cómo puede verse, el resultado devuelto por "Default.aspx" se posiciona automáticamente en la página correcta del GridView.

Como alternativa al control WebBrowser se puede usar un objeto WebClient para "leer" toda la página. Por ejemplo de esta forma:

VB.NET:

Dim rhtml As String
Using w = New WebClient()
rhtml = w.DownloadString(String.Format("http://localhost:49418/Default.aspx?HIscore={0}",    sQueryCifrado))
End using

C#:

string rhtml;
using (var w = new WebClient())
{
rhtml = w.DownloadString(String.Format("http://localhost:49418/Default.aspx?HIscore={0}", sQueryCifrado));
}

De esa forma, en la variable rhtml tendríamos todo el código en formato HTML que devuelve la página "Default.aspx". Lo que deberíamos hacer entonces es "parsear" dicho código HTML y construirnos nosotros de forma manual la tabla de puntuaciones y representarla en nuestro formulario.

Como apunte final, decir que la insercción en la base de datos se realiza a base de parámetros. De esta forma coseguiremos evitar que nos inyecten "código SQL" no deseado en nuestra base de datos y así evitar que nos hagan un estropicio.

 

Tutoriales relacionados:
.NET Tutorial 22. Base de datos (Parte I)
.NET Tutorial 23. Base de datos (Parte II). Access, SQL Server y MySQL: Ejemplo práctico

 

Saludos.
mov eax,ollydbg; Int 13h

Descargar código fuente del .NET Tutorial 50
(36 KB. Visual Studio 2008)