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.
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.
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, 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 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.
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.
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).