Multi-threaded Windows Forms utilizando BindingSource y BackgroundWorker (.NET 2.0)


Una de las cosas que quería hacer en la aplicación de la que ya comente algo en este post, es que en las distintas opciones cuando se haga una operación que pueda llevar tiempo no se bloquee todala aplicación esperando a que esa operación termine para poder seguir trabajando.

Como la idea es usar DataBinding ( usando BindingSource ) contra instancias de clases que representan los datos.

Esas operaciones, por ejemplo obtener datos de una base de datos, actualizarlos, llamar a Web Services, … se tendrían que hacer en hilos ( threads ) a parte … y aquí es donde se plantea el problema.

En .NET no se pueden realizar modificaciones sobre los elementos de la UI desde un hilo que o sea aquel en que se hayan creado.

En su momento no encontré una solución ( probé varias cosas sin éxito alguno … y la verdad es que eran posibilidades bastante complicadas de implementar … juas ).

Al final hace un par de dias ( esto de volver a ver cosas en lo del CAB parece haberme sentado bien …. juas ) se me ocurrio una posibilidad tras estar modificado un formulario, al ver en el código del mismo las llamadas a SuspendLayout y ResumeLayout se me ocurrió irme a la ayuda a ver si el BindingSource tenía algo similar … y … efectivamente SuspendBinding y ResumeBinding … si es que a veces no hay nada como leerse la ayuda …😉

La solución pasa por llamar al método SuspendBinding de la instancia de BindingSource antes de realizar ninguna modificación sobre el objeto que se utiliza para los bindings, realizar las modificaciones que hagan falta ( ojo que no se estaría accediendo directamente a los controles de Windows Forms, sino a los datos asociados a estos ) y una vez que se haya acabado llamar al método ResumeBiding de la instancia de BindingSource.

Para lanzar la operación en un hilo a parte se utilizaría BackgroundWorker🙂

Aquí va un ejemplo que es el mismo código que he usado para hacer las pruebas en la aplicación:

public void Refresh(BindingSource bindingSource)
{
    if (this.IsActive && this.AllowRefresh)
    {
        if (this.ViewState == ViewState.Default)
        {
            // Creamos el BackgroundWorker y lo ejecutamos pasando como parámetro
            // la instancia de BindingSource.
            BackgroundWorker worker = new BackgroundWorker();

            worker.DoWork += new DoWorkEventHandler(OnRefresh);
            worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(OnRefreshCompleted);

            worker.RunWorkerAsync(bindingSource);
        }
    }
}

protected void OnRefresh(object sender, DoWorkEventArgs e)
{
    // Cambiamos el estado a Busy ( se desactivan botones y opciones de menu para
    // la vista, se mantienen así mientras el estado sea Busy)
    this.ViewState = ViewState.Busy;

    // Obtenemos el servicio de persistencia ( NHibernate ahora mismo )
    IPersistenceService persistence = this.WorkItem.Services.Get<IPersistenceService>();

    // Obtenemos la instancia asociada al BindingSource
    TEntity entity = (TEntity)(((BindingSource)e.Argument).Current);

    // Suspendemos los bindings
    ((BindingSource)e.Argument).SuspendBinding();

    // Abrimos la sesión de persistencia y obtenemos los datos
    persistence.Open();
    persistence.Load<TEntity>(entity, persistence.GetKeyValue<TEntity>(entity));

    // Pasamos como resultado el mismo BindingSource que nos llego aqui como parámetro
    e.Result = e.Argument;
}
protected virtual void OnRefreshCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    IPersistenceService persistence = this.WorkItem.Services.Get<IPersistenceService>();

    ((BindingSource)e.Result).ResumeBinding();

    if (persistence != null)
    {
        persistence.Close();
        persistence = null;
    }

    if (e.Error != null)
    {
        #warning Procesar excepcion aqui
    }

    this.ViewState = ViewState.Default;
}

Si se quisiese comprobar que la aplicación no se queda bloqueada esperando y se puede seguir trabajando de forma normal, bastaría con poner un Thread.Sleep(xxxxx) en el mismo sitio en el que se modifiquen los datos ( en el caso del ejemplo en OnRefresh ) y comprobar que al finalizar los datos se presentan en pantalla de forma correcta.

En el ejemplo se utiliza un servicio de persistencia que utiliza NHibernate, la verdad es que no se si usar NHibernate o LLBLGen Pro ( del que tengo licencia regalo del autor de la criatura Frans Bouma :D ) … las dudas me asaltan de mala manera en este tema concreto y amenazan seriamente con no dejarme vivir … juas, para obtener datos de una base de datos y presentarlos en pantalla.


  1. Pingback: Estados de las vistas (I) « Carliños Blog

  2. In DoWork event , I create controls child, and in ProgressChanged event I try add controls to SplitterPanel, but the application not responds. I use InvokeRequired for controls child.

    Any suggestions, please ?

    Private Sub bgCargaFichero_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bgCargaFichero.DoWork

    Try

    ‘SplitContainer1.Panel1.SuspendLayout()

    ‘SplitContainer1.Refresh()

    ‘ Procesar fichero

    Me.ProcesarFicheroCargadoWork() ‘ IN THIS METHOD I CREATE CONTROLS Type = ContenedorVisorBase

    ‘SplitContainer1.Panel1.ResumeLayout(False)

    If bgCargaFichero.CancellationPending = True Then

    e.Cancel = True

    Else

    e.Result = True

    End If

    End Sub

    Private Sub bgCargaFichero_ProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles bgCargaFichero.ProgressChanged

    Dim contenedorPagina As GRUPOBACKUP.Administrador.Util.Cliente.ControlesWindows.ContenedorVisorBase = Nothing

    contenedorPagina = CType(e.UserState, GRUPOBACKUP.Administrador.Util.Cliente.ControlesWindows.ContenedorVisorBase)

    If contenedorPagina IsNot Nothing Then

    AddMiniaturaToPanel(contenedorPagina)

    End If

    =====

    Delegate Sub AddMiniaturaToPanelDelegate2(ByVal cCTL As Control)

    Private Sub AddMiniaturaToPanel(ByVal cCTL As Control)

    If cCTL.InvokeRequired Then

    Dim d As New AddMiniaturaToPanelDelegate2(AddressOf AddMiniaturaToPanel)

    Me.Invoke(d, New Object() {cCTL})

    Else

    SplitContainer1.Panel1.Controls.Add(cCTL) ‘ Here, the application not responds !!!!

    End If

    End Sub

  3. Leí en un foro que:

    I read somewhere that SuspendBinding and ResumeBinding don’t work properly for DataGridViews because it involves ‘complex data-binding’?

    Saludos.


Responder

Por favor, inicia sesión con uno de estos métodos para publicar tu comentario:

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