En este ejemplo veremos cómo asignar a un control de un formulario un texto o asignar cualquier propiedad desde otro hilo diferente al usado por el formulario. En Visual Studio 2005 (realmente en .NET Framework 2.0) hacer esto produce un error, algo que no ocurría en .NET 1.x (Visual Studio .NET). Y veremos cómo solucionarlo de dos formas: una que solo vale para VS 2005 y la otra que es válida para todas las versiones de Visual Studio (.NET Framework). El código de ejemplo, como de costumbre, está en los dos lenguajes "favoritos": Visual Basic y Visual C#.
|
|
Introducción
Cuando trabajamos con Visual Studio 2005 (ya sea con VB 2005 o C# 2005) y usamos un hilo (thread) para ejecutar cualquier código y desde ese código se intenta acceder a un control del formulario, recibiremos este error (ver figura 1):
- Cross-thread operation not valid: Control '<el control>' accessed from a thread other than the thread it was created on.
Este error no se producía en las versiones anteriores de Visual Studio .NET.
Figura 1. El error al acceder a un control desde otro hilo
¿Cuándo se produce ese error?
El error se produce cuando iniciamos un nuevo hilo (thread) y desde ese hilo intentamos acceder a un control del formulario, por ejemplo, suponiendo que tenemos un formulario con un botón y una caja de textos, si escribimos lo siguiente y al iniciar la aplicación pulsamos en el botón, el cual iniciará un nuevo hilo, y desde el método usado como punto de inicio de ese nuevo hilo queremos asignar una cadena a la propiedad Text de la caja de textos, obtendremos el error mostrado en la figura 1.
El código para VB 2005 y C# 2005 es el siguiente:
Private Sub Button1_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles Button1.Click Dim t As New Thread(AddressOf enOtroHilo) t.Start() End Sub Private Sub enOtroHilo() Me.TextBox1.Text = "Esto dará error" End Subprivate void button1_Click(object sender, EventArgs e) { Thread t = new Thread(enOtroHilo); t.Start(); } private void enOtroHilo() { this.textBox1.Text = "Esto dará error"; }
Para solucionarlo podemos hacer varias cosas, la primera y más "fácil" es indicar al .NET que no tenga en cuenta ese error, de esta forma obtendremos el mismo comportamiento que en la versiones anteriores de Visual Studio.
El truco consiste en asignar un valor falso a la propiedad CheckForIllegalCrossThreadCalls:CheckForIllegalCrossThreadCalls = FalseCheckForIllegalCrossThreadCalls = false;Esa asignación la podemos poner en el evento Load del formulario.
Una forma igualmente válida para Visual Studio 2003
Otra forma de solucionarlo es creando un delegado y usando el método Invoke del formulario, de forma que el formulario se encargará de llamar al método apuntado por el delegado, de forma que podamos asignar al control el texto que queramos, en este caso no se producirá una excepción ya que el hilo es el mismo que el usado por el formulario.
¿Por qué complicarnos?
Porque esta forma de hacerlo, aunque es menos genérica que la anterior, nos servirá para todas las versiones de Visual Studio, y si la vas a usar en Visual Studio 2003, pues mejor, ya que se supone que el problema que se intenta arreglar en Visual Studio 2005 al avisar de ese "error" es porque en realidad puede ser "perjudicial" para nuestro código... vamos, que si lo han corregido... ¡por algo será!
El código que tendremos que usar es más complicado que el anterior, al menos si no estás acostumbrado a usar delegados, (la gente de C# se supone que "deberían" estar acostumbrados a usar delegados porque no tienen más remedio... je, je, bueno, al menos si quieren crear clases que produzcan eventos y esas cosas.
Tengo que decirte que el código que voy a mostrarte está "inspirado" en uno de los ejemplos de Visual Studio 2005 propone para solucionar ese error.
Lo que se hace es definir un delegado con el tipo de método que usaremos para hacer la asignación al control, desde el método llamado por el nuevo hilo usamos ese método en lugar de asignar directamente la cadena al textbox. En el método se comprueba si el valor devuelto por la propiedad InvokeRequired es verdadero, si es así, quiere decir que el hilo en el que está actualmente esa llamada es diferente al hilo en el que se creó el control, por tanto, creamos un nuevo objeto del tipo del delegado, le indicamos que método debe usar y a continuación llamamos al método Invoke de nuestro formulario, cuando se entre nuevamente en ese método (porque el formulario lo "invoca") el valor que devolverá la propiedad InvokeRequired será False, ya que el hilo en el que está es el mismo en el que se creó el control, ya que es el mismo hilo usado por el formulario... vamos, como si entráramos por la puerta falsa... De esta forma se podrá asignar el valor a la propiedad Text, ya que estamos usando el mismo hilo.
Private Sub Button1_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles Button1.Click Dim t As New Thread(AddressOf enOtroHilo) t.Start() End Sub Private Sub enOtroHilo() 'Me.TextBox1.Text = "Esto dará error" ' En lugar de asignar directamente el texto ' llamamos al método SetText SetText("Esto NO dará error") End Sub ' Usando delegados y funciones "callback" ' ' Este delegado define un método Sub ' que recibe un parámetro de tipo String Delegate Sub SetTextCallback(ByVal text As String) Private Sub SetText(ByVal text As String) ' InvokeRequired required compares the thread ID of the ' calling thread to the thread ID of the creating thread. ' If these threads are different, it returns true. If TextBox1.InvokeRequired Then Dim d As New SetTextCallback(AddressOf SetText) Me.Invoke(d, New Object() {text}) Else Me.TextBox1.Text = text End If End Sub
private void button1_Click(object sender, EventArgs e) { Thread t = new Thread(enOtroHilo); t.Start(); } private void enOtroHilo() { // this.textBox1.Text = "Esto dará error"; // En lugar de asignar directamente el texto // llamamos al método SetText SetText("Esto NO dará error"); } // Usando delegados y funciones "callback" // // Este delegado define un método void // que recibe un parámetro de tipo string delegate void SetTextCallback(string text); private void SetText(string text) { // InvokeRequired required compares the thread ID of the // calling thread to the thread ID of the creating thread. // If these threads are different, it returns true. if( textBox1.InvokeRequired ) { SetTextCallback d = new SetTextCallback(SetText); this.Invoke(d, new object[] { text }); } else { this.textBox1.Text = text; } }
¿Y si el método que llamo desde el hilo está en otra clase que debe devolver valores para asignarlo, por ejemplo a un ListBox?
Lo aquí explicado es válido también si el método al que llamamos desde el nuevo hilo está en una clase, y si queremos que lo procesado por el método de esa clase lo asignemos a un ListBox, (esto viene al caso de una consulta en los foros de C# de mi sitio, ID del mensaje: 13030), tendremos que hacer lo siguiente:
- Definir un evento en la clase, de forma que cada vez que haya que devolver un valor, lo hagamos mediante ese evento
- En el formulario interceptamos el evento de forma que cada vez que se produzca llamemos al método del delegado "callback" (como en el ejemplo anterior), y desde ese método añadimos la cadena recibida al ListBox
Como podemos comprobar en el código, hacemos casi lo mismo que antes, pero he querido poner también este ejemplo, para que tengamos otra visión diferente y sepamos aplicarlo en casos que parecen distintos, pero en el fondo es lo mismo... o casi.
Class Form1 ' ... Private Sub btnIniciar_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles btnIniciar.Click ListBox1.Items.Add("---") Dim cmh As New ClaseMultiHilo() AddHandler cmh.Mensaje, AddressOf cmh_Mensaje cmh.parametro = 2 Dim t As New Thread(AddressOf cmh.MostrarMensaje) t.Start() End Sub Delegate Sub SetTextCallback2(ByVal text As String) Private Sub SetText2(ByVal text As String) If ListBox1.InvokeRequired Then Dim d As New SetTextCallback2(AddressOf SetText2) Me.Invoke(d, New Object() {text}) Else Me.ListBox1.Items.Add(text) End If End Sub Private Sub cmh_Mensaje(ByVal p As String) SetText2(p) End Sub End Class ' Clase para lanzar en un hilo Class ClaseMultiHilo Public Delegate Sub MensajeDelegate(ByVal p As String) Public Event Mensaje As MensajeDelegate Public parametro As Integer Public Sub MostrarMensaje() For i As Integer = 0 To parametro - 1 OnMensaje("Parametro " & i) Next End Sub Protected Sub OnMensaje(ByVal p As String) RaiseEvent Mensaje(p) End Sub End Class
private void btnIniciar_Click(object sender, System.EventArgs e) { this.listBox1.Items.Add("---"); ClaseMultiHilo cmh = new ClaseMultiHilo(); cmh.Mensaje += new ClaseMultiHilo.MensajeDelegate(cmh_Mensaje); cmh.parametro = 2; ThreadStart ts = new ThreadStart(cmh.MostrarMensaje); Thread t = new Thread(ts); t.Start(); } private void cmh_Mensaje(string p) { // this.listBox1.Items.Add(p); this.SetText2(p); } delegate void SetTextCallback2(string text); private void SetText2(string text) { if(this.listBox1.InvokeRequired) { SetTextCallback2 d = new SetTextCallback2(SetText2); this.Invoke(d, new object[] { text }); } else { this.listBox1.Items.Add(text); } } // Clase para lanzar en un hilo class ClaseMultiHilo { public delegate void MensajeDelegate(string p); public event MensajeDelegate Mensaje; public int parametro; public void MostrarMensaje() { for(int i = 0; i < parametro; i++) OnMensaje("Parametro " + i); } protected void OnMensaje(string p) { if( Mensaje != null) Mensaje(p); } }
Y esto es todo amigos... que lo "hiles" bien ;-)))
Nos vemos.
Guillermo