C# - Dispatcher.Invoke

Descripción general

Cuando intentamos que una tarea que se ejecuta en segundo plano (en un hilo) se comunique con la interfaz de usuario para manejar cualquier control (por ejemplo una barra de estado) aparece un error que lo impide.

[TOC] Tabla de Contenidos


↑↑↑

C# - Dispatcher.Invoke


↑↑↑

Introducción

Cuando intentamos que una tarea que se ejecuta en segundo plano (en un hilo) se comunique con la interfaz de usuario para manejar cualquier control (por ejemplo una barra de estado) aparece un error y no se puede hacer.

El problema a es que el modelo de seguridad que implementa WPF compartimenta los subprocesos e impide que cualquier subproceso distinto al que crea el objeto de la interfaz de usuario, (por ejemplo un control de la ventana) pueda acceder al mismo.

Esta restricción impide que dos o más subprocesos puedan tomar el control de la entrada de datos del usuario o modificar esos mismos datos en la pantalla porque podría invalidarlos.

Problema a modo de ejemplo

Supongamos que queremos implementar un proceso [Save] en un subproceso independiente, porque es un proceso muy largo y se bloquearía la interfaz de usuario.

El problema surge a la hora de informar al usuario de que el proceso ha terminado. Podríamos mostrar una ventana emergente, pero tiene el problema de que distrae y no queda bien, también podríamos usar una barra de progreso o como vamos a usar en este ejemplo poner un mensaje en la barra de estado informando de lo que ha pasado.


↑↑↑

La clase HiloSaveFichero

Para implementar el proceso sabe en un hilo, lo primero que necesitamos es una clase a la que llamaremos [HiloSaveFichero] que envuelva el proceso Hilo y que contenga la información que necesita el hilo para funcionar, es decir, el nombre del fichero y el texto que se va a guardar. Ambos datos estarán como campos de la clase y se cargan a través del constructor. Se supone que sabemos cómo se hace y no voy a escribir ese código.

La función que se encarga de guardar el texto en el fichero puede ser más o menos así:

// ****************[class HiloSaveFichero] ***************

// Los hilos No tienen parámetros
// Los hilos no devuelven nada
public void SaveFicheroConTread()
{
    try
    {
        SaveFichero();
    }
    catch (Exception ex)
    {
        TareaTerminada = false;
        OnErrorProcesoSaveFichero(ex);
    }
}

private bool SaveFichero()
{
    TareaTerminada = false;
 
    using (FileStream fs = new FileStream(NombreCompletoArchivo,
                                FileMode.OpenOrCreate,
                                FileAccess.Write,
                                FileShare.None))
    {
        using (StreamWriter sw = new StreamWriter(fs))
        {
            sw.Write(TextoAgrabar);
        }
    }
    TareaTerminada = true;
    OnProcesoSaveFicheroCompletado();
 
    return TareaTerminada;
}

Un hilo necesita usar eventos para informar, fundamentalmente de dos cosas, de que el proceso ha terminado sin problemas o de que ha ocurrido un error y el proceso no se ha terminado correctamente. Y de eso se encargan dos eventos (cuyo código tampoco escribo)

****************[    class ErrorSaveFicheroEventArgs] ***************

// evento personalizado
class ErrorSaveFicheroEventArgs : EventArgs
{
    public Exception ex { get; set; }
    public string NombreCompletoArchivo { get; set; }
    public bool TareaTerminada { get; set; }
 
    public ErrorSaveFicheroEventArgs(Exception ex)
    {
        this.ex = ex;
        NombreCompletoArchivo = string.Empty;
        TareaTerminada = false;
    }
}

****************[    class HiloSaveFichero] ***************
 
#region "Patrón de implementación del evento Personalizado "
 
// A) Declarar el delegado de un evento personalizado
public delegate void ErrorSaveFicheroEventHandler(object sender, ErrorSaveFicheroEventArgs e);
 
// B) Declarar el evento
public event ErrorSaveFicheroEventHandler ErrorProcesoSaveFichero;
 
// C) Función OnEvento para disparar el evento
protected virtual void OnErrorProcesoSaveFichero(Exception ex)
{
    if (ErrorProcesoSaveFichero != null)
    {
        ErrorSaveFicheroEventArgs personalizadoEventArgs = new ErrorSaveFicheroEventArgs(ex);
        personalizadoEventArgs.NombreCompletoArchivo = this.NombreCompletoArchivo;
        personalizadoEventArgs.TareaTerminada = this.TareaTerminada;
 
        ErrorProcesoSaveFichero(this, personalizadoEventArgs);
    }
}
 
// D) Funciones para Suscribirse / Borrarse de la notificacion del evento
public void EventoErrorProcesoSaveFicheroSuscribirse(ErrorSaveFicheroEventHandler metodoQueSeAñade)
{
    ErrorProcesoSaveFichero += metodoQueSeAñade;
}
public void EventoErrorProcesoSaveFicheroQuitarSuscripcion(ErrorSaveFicheroEventHandler metodoQueSequita)
{
    ErrorProcesoSaveFichero -= metodoQueSequita;
}
 
#endregion

↑↑↑

El proceso Cliente (La ventana del formulario)

El proceso Cliente, el que va a lanzar el hilo, suponemos que es un formulario y que va a funcionar como consecuencia de que se pulsa un botón o el menú correspondiente a la operación [Save]

****************[    class MainWindow] ***************
#region " Save con Hilos Thread"
 
 
private void AccionBotonSaveHilosTread()
{
    // declarar e instanciar el objeto hilo
    objHiloSave = new HiloSaveFichero(this.TextBoxNombreFichero.Text, this.TextBoxTextoCualquiera.Text);
    // activar el escuchador de eventos del hilo
    objHiloSave.EventoErrorProcesoSaveFicheroSuscribirse(HiloSaveFichero_ErrorProcesoSaveFichero);
    objHiloSave.EventoProcesoSaveFicheroCompletadoSuscribirse(HiloSaveFichero_ProcesoSaveFicheroCompletado);
 
    System.Threading.Thread hiloTrabajoSave = new System.Threading.Thread(objHiloSave.SaveFicheroConTread);
           
    hiloTrabajoSave.Start();
            }
 
 
 
void HiloSaveFichero_ErrorProcesoSaveFichero(object sender, ErrorSaveFicheroEventArgs e)
{
    AccionEscribeEnBarraEstado(e.ex.Message);
}
 
void HiloSaveFichero_ProcesoSaveFicheroCompletado(object sender, EventArgs e)
{
    AccionEscribeEnBarraEstado("Terminado - AccionBotonSaveHilosTread");
}
        
 
#endregion

Bien ahora es cuando viene la madre del cordero, la función [AccionEscribeEnBarraEstado] se encarga de escribir un mensaje en la barra de estado. El problema es que al estar llamado por un evento disparado por otro hilo el modelo de seguridad de WPF impide su ejecución y se dispara una excepción.

****************[    class MainWindow] ***************

private void AccionEscribeEnBarraEstado(string texto)
{
    statusBarItemInformes.Content = texto;
}

↑↑↑

La función Dispatcher

La forma de resolver este problema es usar delegados y la función Dispatcher

****************[    class MainWindow] ***************

// A) Función que será llamada por el delegado -> [void AccionEscribeEnBarraEstado(string texto)]
// B) Declarar el delegado (termina en Callback según convenciones de Microsoft
private delegate void actualizaBarraEstadoCallback(string texto);
 
 
// Usar el método DispatcherObject.CheckAccess para determinar si 
// la llamada se realiza desde este proceso o desde otro hilo
private void TryToEscribeEnBarraEstado(string texto)
{
 
    if (this.statusBarItemInformes != null)
    {
        // Comprobar si este hilo tiene acceso al objeto
        if (this.statusBarItemInformes.CheckAccess())
        {
            // Si el hilo tiene acceso al objeto
            AccionEscribeEnBarraEstado(texto);
        }
        else
        {
            // No el hilo no tiene acceso al objeto
            // Utilizar el método Dispatcher.Invoke
            // C) Declarar las variables delegado
            actualizaBarraEstadoCallback actualizaBarraEstado = null;
            // D) Instanciar la variable delegado
            actualizaBarraEstado = new actualizaBarraEstadoCallback(AccionEscribeEnBarraEstado);

             // Usar Dispatcher
            object[] args = { texto };
            statusBarItemInformes.Dispatcher.Invoke(
                actualizaBarraEstado, DispatcherPriority.ApplicationIdle, args);
 
        }
    }
}


private void AccionEscribeEnBarraEstado(string texto)
{
    statusBarItemInformes.Content = texto;
}

#Region "Dispatcher.Invoke  en VB"

    '// A) Función que será llamada por el delegado ->
    '      [void AccionEscribeEnBarraEstado(string texto)]
    '// B) Declarar el delegado 
    '      (termina en Callback según convenciones de Microsoft)
    Private Delegate Sub actualizaBarraEstadoCallback(texto As String)


    '// Usar el método DispatcherObject.CheckAccess para determinar si 
    '// la llamada se realiza desde este proceso o desde otro hilo
    Private Sub TryToEscribeEnBarraEstado(texto As String)
        If (Not (Me.statusBarItemInformes Is Nothing)) Then

            ' // Comprobar si este hilo tiene acceso al objeto
            If Me.statusBarItemInformes.CheckAccess() Then

                ' // Si el hilo tiene acceso al objeto
                AccionEscribeEnBarraEstado(texto)
            Else
                '// No el hilo no tiene acceso al objeto
                '// Utilizar el método Dispatcher.Invoke
                '// C) Declarar las variables delegado
                Dim actualizaBarraEstado As actualizaBarraEstadoCallback = Nothing
                '// D) Instanciar la variable delegado
                actualizaBarraEstado = New actualizaBarraEstadoCallback(AddressOf AccionEscribeEnBarraEstado)

                ' // Usar Dispatcher
                Dim args() As Object = {texto}
                statusBarItemInformes.Dispatcher.Invoke(
                    actualizaBarraEstado, DispatcherPriority.ApplicationIdle, args)

            End If
        End If
    End Sub


    Private Sub AccionEscribeEnBarraEstado(texto As String)
        statusBarItemInformes.Content = texto
    End Sub

#End Region

El objeto [Dispatcher] sirve para solicitar que el subproceso de la interfaz de usuario ejecute un método por encargo de otro subproceso.

El objeto [Dispatcher] encola estas solicitudes y las ejecuta en la interfaz de usuario en el momento preciso

Para acceder al objeto [Dispatcher] hay que usar a la propiedad [Dispatcher] de cualquier objeto del formulario ventana, incluyendo, evidentemente, el propio objeto ventana.

Para enviar una solicitud al objeto [Dispatcher] se utiliza el método [Invoke]. Este método está sobrecargado, pero todas las sobrecargas esperan un objeto [Delegate] que encapsule la referencia al método que el objeto [Dispatcher] tendrá que ejecutar.


↑↑↑

Mejorando el código

Existe una forma más sencilla de solucionar este problema usando funciones lambda, y entonces la función anterior quedaría así:

// Usar el metodo DispatcherObject.CheckAccess para determinar si 
// la llamada se realiza desde este proceso o desde otro hilo
private void AccionEscribeEnBarraEstado(string texto)
{
 
    if (this.statusBarItemInformes != null)
    {
        // Checking si este hilo tiene acceso al objeto
        if (this.statusBarItemInformes.CheckAccess())
        {
            // si el hilo tiene acceso al objeto
            statusBarItemInformes.Content = texto;
        }
        else
        {
            Action accioncita = new Action(() => statusBarItemInformes.Content = texto);
            this.Dispatcher.Invoke(accioncita, DispatcherPriority.ApplicationIdle);
        }
    }
}

El mismo codigo en Visual basic

'Usar el metodo DispatcherObject.CheckAccess para determinar si 
 'la llamada se realiza desde este proceso o desde otro hilo
 Private Sub AccionEscribeEnBarraEstado1(texto As String)
     If (Not (Me.statusBarItemInformes Is Nothing)) Then

         'Comprobar si este hilo tiene acceso al objeto
         'If Me.statusBarItemInformes.CheckAccess() = False Then
         If Me.statusBarItemInformes.CheckAccess() Then
             ' // Si el hilo tiene acceso al objeto
             statusBarItemInformes.Content = texto
         Else
             ' No el hilo no tiene acceso al objeto
             ' Utilizar el método Dispatcher.Invoke
             Dim accioncita As Action = New Action(Sub() statusBarItemInformes.Content = texto)
             Me.Dispatcher.Invoke(accioncita, DispatcherPriority.ApplicationIdle)
         End If
     End If
 End Sub
 

De esta forma la propia función [AccionEscribeEnBarraEstado] es la que invoca al método [Dispatcher.Invoke] si es necesario.

El método [Invoque] espera una solicitud en forma de parámetro [Delegate] que le señale el método que debe ejecutar, Sin embargo, [Delegate] es un clase abstracta, mientras que la clase [Action] es una implementación concreta de la clase [Delegate] diseñada para hacer referencia a un método que NO acepte parámetros y no devuelve resultados.


↑↑↑

Los delegados genéricos: Action y Func

Podemos utilizar los siguientes delegados genéricos en lugar de delegate. Con ellos conseguimos una sintaxis algo más refinada y simple.

Action se utiliza para aquellas expresiones lambda que no retornan ningún valor.

Action<string> saludo = s => Console.Write("Hola {0}!", s);
saludo("Amigo");

Func para aquellas expresiones que retornen un valor.

Func<int, int, int> suma = (a, b) => a + b;
int resultado = suma(3, 5);

Tanto en el caso de Func como en Action el número máximo de parámetros permitido no es ilimitado, hay que consultar con la ayuda MSDN para salir de dudas :) . Func tiene un parámetro más porque el último parámetro es el valor de retorno).


↑↑↑

A.2.Enlaces

[Para saber mas]
[Grupo de documentos]
[Documento Index]
[Bloque de apuntes tácticos de C#]
[Documento Start]
[Imprimir el Documento]