Palabras sobre código

Abril 25, 2009

Trabajos en hilos separados con BackgroundWorker (parte 2, reusabilidad)

Archivado en: Uncategorized — Etiquetas:, , , , — David Lay @ 3:03 am

En la parte 1 mostré como crear una barra de progreso que realizaba un trabajo de manera asíncrona mientras mantenía a el usuario informado de que se estaba realizando un trabajo. Todo eso fue hecho en forma de prototipo, y con varias cosas pendientes que mejorar.

En esta segunda parte me voy a enfocar en la reusabilidad del código,  explicando una mejor forma utilizar esta ventana, y voy a explicar el manejo de exepciones. Todo esto acompañado de pequeñas modificaciones al código del post anterior.

Ventana principal del programa

Ventana principal del programa

Nuestro proyecto consta de un formulario principal, que tiene un datagridview y un botón, el que llama a un diálogo que presenta una barra de progreso y se encarga de lanzar un trabajo a un hilo por separado, cerrandose al terminar ese trabajo. El trabajo a realizar, es un método dentro del mismo formulario principal, lo que nos impide pasar parámetros o recibir resultados de una forma limpia y estamos usando variables globales.

Tampoco tenemos manejo de excepciones, ya que al ocurrir cualquier problema, la ventana se cierra sin dar ninguna explicación, aunque la aplicación no se cae.

Encapsulación

En la mayoría de los casos nuestro método de trabajo va a estar en una clase especialmente diseñada para realizar la obtención de los datos y la preparación de estos, ya que pueden venir desde cualquier fuente externa.

Nuestro siguiente paso es precisamente emular este escenario creando una clase llamada RSSReader.cs que va a encapsular la lógica sobre la obtención de un RSS a través de HTTP.

    class RSSReader
    {
        public Uri Ubicación
        {
            get;
            set;
        }
        public DataTable Items
        {
            get;
            private set;
        }
        public String Título
        {
            get;
            private set;
        }
        public String Descripción
        {
            get;
            private set;
        }

        public RSSReader(String url)
        {
            Ubicación = new Uri(url);
        }

        public void LeeRSS()
        {
            var noticias = new DataSet();
            noticias.ReadXml(Ubicación.AbsoluteUri, XmlReadMode.Auto);
            Título = noticias.Tables["channel"].Rows[0]["title"].ToString();
            Descripción = noticias.Tables["channel"].Rows[0]["description"].ToString();
            Items = noticias.Tables["item"];
        }

    }

De esta forma podemos apreciar una pequeña clase que nos va a facilitar el paso de parámetros y la obtención de resultados.

El siguiente paso es modificar el formulario para que soporte nuestra nueva manera de obtener los datos:

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            var rssReader = new RSSReader("http://lifehacker.com/tag/top/index.xml");
            var proceso = new dlgDialogoProceso(rssReader.LeeRSS);
            proceso.Mensaje = "Obteniendo noticias de Lifehacker...";
            if (proceso.ShowDialog(this) == DialogResult.OK)
                dgLinks.DataSource = rssReader.Items;
            dgLinks.Refresh();
        }
    }

Mucho más limpio y claro, sin manejo de datasets ni lecturas de xml. Todo eso está correctamente encapsulado en la nueva clase.

¿Recuerdan que nuestro diálogo recibía una función que debía ejecutar de manera asíncrona? En nuestra versión anterior, ese método estaba dentro del mismo formulario. Ahora nosotros queremos hacer responsable a la clase RSSReader de todo el trabajo de obtención de datos, esto significa, que debemos pasarle al dialogo una función de un objeto RSSReader.

Manejo de Excepciones

Lo que sigue es el manejo de excepciones.  Este problema radica en que no se pueden manejar exepciones de un hilo a otro con bloques try-catch, ya que las excepciones están almacenadas en pilas, y las pilas están alocadas en memoria que es independiente para cada proceso.

Pero nuestro backgroundworker nos tiene eso solucionado, hasta cierto punto. Cuando nuestro proceso termina de ejecutarse, el backgroundworker llama al evento RunWorkerCompleted, ya sea que haya terminado debido a una excepción o de manera normal.

La diferenciación la realiza gracias a los parámetros, veamos el código primero y luego lo explicamos (He colocado solo las partes que han cambiado desde la versión anterior):

    public partial class dlgDialogoProceso : Form
    {
        [ ... ]

        /// <summary>Entrega información sobre la excepción que ha generado el trabajo que se realizaba</summary>
        public Exception Error
        {
            get; private set;
        }

        [ ... ]

        /// <summary> Configura un BackgrowndWorker para realizar el trabajo indicado</summary>
        private BackgroundWorker CreaTrabajador(trabajo delegado)
        {

            [ ... ]

            // Definimos que debe hacer al concluir el trabajo
            trabajador.RunWorkerCompleted += delegate(object sender, RunWorkerCompletedEventArgs e) {
                 if (e.Error == null) {
                     DialogResult = DialogResult.OK;
                 }
                 else {
                     // Si ha ocurrido una excepción, la exponemos
                     Error = e.Error;
                     // y reportamos que ha ocurrido un error
                     DialogResult = DialogResult.Abort;
                 }
                 Close();
             };

            // Devolvemos el trabajador configurado
            return trabajador;
        }

    }

He agregado una propiedad que expone una excepción si se encuetra, y luego he cambiado el delegado que maneja el término del trabajo para revisar si ha ocurrido una exepción.

Importante es notar, que en el método que realiza el trabajo (en este caso LeeRSS) no debe haber ningún try-catch para que la excepción suba hasta el método DoWork y luego el backgrowndworker sea capaz de manejarla.

Un problema con esto, es que el debugger de visual studio siempre va a parar en el punto donde cree que se ha generado una excepción no manejada, pero tengan confianza que esto funciona correctamente en ejecución normal.

Si les molesta mucho este problema del debugger, pueden agregar el atributo DebuggerStepThrough del namespace System.Diagnostics tanto en el método que realiza el trabajo (LeeRSS) como en la clase del diálogo. Este atributo evita que el debugger entre en el código, pero por lo mismo, no va a parar incluso si han ubicado ahi un breakpoint.

Con esto concluimos la seguda parte de esta serie de posts introductorios para el trabajo con backgroundworker.

El código completo del proyecto lo pueden bajar aqui.

Me había comprometido con una tercera parte, pero no la voy a realizar. En cambio voy a generar una cantidad de nuevos posts sobre Windows Comunication Framework, NHibernate, ASP MVC, ya que estoy en un proyecto universitario que está explorando todas estas tecnologías.

También pueden esperar posts sobre Test Driven Design y Pruebas unitarias. Estén atentos.

Aún no hay comentarios »

Aún no hay comentarios.

Canal RSS de los comentarios de la entrada. URI para TrackBack.

Deja un comentario

Blog de WordPress.com.